fix(ui): 可展开或已在界面上的文案不再弹出气泡
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -33,12 +33,36 @@ function getVisibleText(root: Element): string {
|
|||||||
return norm(parts.join(" "));
|
return norm(parts.join(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 是否存在省略截断(含 line-clamp) */
|
/** 原地展开全文控件:文案须是「展开/收起/展开全部…」,不含「展开 N 条回复」 */
|
||||||
function isVisuallyTruncated(root: Element): boolean {
|
function isFullTextExpandControl(el: Element): boolean {
|
||||||
const nodes: Element[] = [root, ...Array.from(root.querySelectorAll("*"))];
|
const t = (el.textContent || "").replace(/\s+/g, " ").trim();
|
||||||
for (const el of nodes) {
|
if (!t) return false;
|
||||||
if (!(el instanceof HTMLElement)) continue;
|
if (t === "展开" || t === "收起") return true;
|
||||||
if (el.clientWidth <= 0 && el.clientHeight <= 0) continue;
|
if (/^(展开全部|查看全文|显示全部|显示全文)/.test(t)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 悬浮目标已能原地展开全文(有 data-expandable,或同级紧随展开按钮)。
|
||||||
|
* 此时气泡只是复述,不再弹出。
|
||||||
|
*/
|
||||||
|
function hasFullTextExpand(host: Element): boolean {
|
||||||
|
if (host.closest("[data-expandable]")) return true;
|
||||||
|
const scope = host.parentElement;
|
||||||
|
if (!scope) return false;
|
||||||
|
for (const btn of scope.querySelectorAll("button, [role='button']")) {
|
||||||
|
if (host.contains(btn)) continue;
|
||||||
|
if (btn.parentElement !== scope) continue;
|
||||||
|
const pos = host.compareDocumentPosition(btn);
|
||||||
|
if (!(pos & Node.DOCUMENT_POSITION_FOLLOWING)) continue;
|
||||||
|
if (isFullTextExpandControl(btn)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单节点是否视觉截断(省略号 / nowrap / line-clamp) */
|
||||||
|
function elementIsTruncated(el: HTMLElement): boolean {
|
||||||
|
if (el.clientWidth <= 0 && el.clientHeight <= 0) return false;
|
||||||
const st = getComputedStyle(el);
|
const st = getComputedStyle(el);
|
||||||
const ellipsis = st.textOverflow === "ellipsis" || el.classList.contains("truncate");
|
const ellipsis = st.textOverflow === "ellipsis" || el.classList.contains("truncate");
|
||||||
const nowrapHidden =
|
const nowrapHidden =
|
||||||
@@ -50,24 +74,55 @@ function isVisuallyTruncated(root: Element): boolean {
|
|||||||
if (el.scrollWidth > el.clientWidth + 1) return true;
|
if (el.scrollWidth > el.clientWidth + 1) return true;
|
||||||
}
|
}
|
||||||
if (clamped && el.scrollHeight > el.clientHeight + 1) return true;
|
if (clamped && el.scrollHeight > el.clientHeight + 1) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提示是否在补全某段被截断的文字(data-tip 须等于该节点全文) */
|
||||||
|
function tipCompletesTruncation(root: Element, tip: string): boolean {
|
||||||
|
const nTip = norm(tip);
|
||||||
|
if (!nTip) return false;
|
||||||
|
const nodes: Element[] = [root, ...Array.from(root.querySelectorAll("*"))];
|
||||||
|
for (const el of nodes) {
|
||||||
|
if (!(el instanceof HTMLElement) || !elementIsTruncated(el)) continue;
|
||||||
|
const t = getVisibleText(el);
|
||||||
|
if (t && norm(t) === nTip) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 去掉箭头等连接符后的词元;用于判断提示是否已画在界面上 */
|
||||||
|
function tipTokens(s: string): string[] {
|
||||||
|
return s
|
||||||
|
.replace(/[→←↔\-–—>|/·•]/g, " ")
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((x) => x.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 可见文字已覆盖提示(含「甲 → 乙」vs 已画出的甲、乙) */
|
||||||
|
function visibleCoversTip(visible: string, tip: string): boolean {
|
||||||
|
const nVis = norm(visible);
|
||||||
|
const nTip = norm(tip);
|
||||||
|
if (!nTip) return true;
|
||||||
|
if (nVis === nTip || nVis.includes(nTip)) return true;
|
||||||
|
const tokens = tipTokens(nTip);
|
||||||
|
if (tokens.length === 0) return false;
|
||||||
|
return tokens.every((tok) => nVis.includes(tok));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅在以下场景显示气泡:
|
* 仅在以下场景显示气泡:
|
||||||
* 1. 提示文案与可见内容不一致(且提示未完整出现在可见文字中)
|
* 1. 提示文案与可见内容不一致(且提示未完整出现在可见文字中)
|
||||||
* 2. 停留目标无可见文字(图标等)
|
* 2. 停留目标无可见文字(图标等)
|
||||||
* 3. 可见内容被省略截断,需要补全
|
* 3. 可见内容被省略截断,且 data-tip 正是那段全文
|
||||||
|
* 例外:目标已能原地展开全文时不弹出;提示词已全部出现在界面上时不弹出
|
||||||
*/
|
*/
|
||||||
function shouldShowTip(host: Element, tip: string): boolean {
|
function shouldShowTip(host: Element, tip: string): boolean {
|
||||||
if (isVisuallyTruncated(host)) return true;
|
if (hasFullTextExpand(host)) return false;
|
||||||
|
if (tipCompletesTruncation(host, tip)) return true;
|
||||||
const visible = getVisibleText(host);
|
const visible = getVisibleText(host);
|
||||||
if (!visible) return true;
|
if (!visible) return true;
|
||||||
const nTip = norm(tip);
|
if (visibleCoversTip(visible, tip)) return false;
|
||||||
const nVis = visible;
|
|
||||||
if (nTip === nVis) return false;
|
|
||||||
if (nVis.includes(nTip)) return false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user