ElementSelectionOverlay
element-selection-overlay元素选择叠加层 · 在容器或同源 iframe 里 hover 高亮/点击选中元素并回吐组件树路径 · 不往目标文档写任何样式 · marked(data-hulian-path) 与 structural 两层路径且对外暴露可靠度 · 跨源 iframe 明确报错不装接上了 · 指向编辑基础设施
用法
基础用法(标记驱动)
预览树自己打了 data-hulian-path / data-hulian-component,选中即回吐标记路径;highlightSelector 把选择粒度限定到组件级。
指向编辑预览区
把指针移进来,点任意一块即可选中。
本月收入
¥ 128,400
订单数
1,204
—悬停:—const [root, setRoot] = useState<HTMLDivElement | null>(null);
const [selected, setSelected] = useState<string | null>(null);
<div ref={setRoot}>
<div data-hulian-component="Hero" data-hulian-path="App/Hero">…</div>
<div data-hulian-component="CtaBar" data-hulian-path="App/Cta">…</div>
</div>
<ElementSelectionOverlay
target={root}
highlightSelector="[data-hulian-component]"
selectedPath={selected}
onSelect={(path) => setSelected(path)}
onClear={() => setSelected(null)}
/>无标记回退结构化路径
预览树没打标记时,路径退化为可被 querySelector 反查的 CSS 选择器(div > section:nth-of-type(2) > button)。
第二段里有一个按钮:
—悬停:—<ElementSelectionOverlay
target={root}
selectedPath={selected}
onSelect={(path) => setSelected(path)}
/>同源 iframe 预览
target 传 iframe 元素即可接管其文档;框画在宿主层,坐标已叠加 iframe 偏移。跨源 iframe 不支持,会走 onError。
—悬停:—const [frame, setFrame] = useState<HTMLIFrameElement | null>(null);
<iframe ref={setFrame} srcDoc={html} title="预览" />
<ElementSelectionOverlay
target={frame}
onSelect={(path) => setSelected(path)}
onError={(e) => console.warn(e.code, e.message)}
/>关掉标签 / 退出选择模式
showLabel=false 只留框;enabled=false 停止拾取与点击拦截,已选中的框仍保留。
指向编辑预览区
把指针移进来,点任意一块即可选中。
本月收入
¥ 128,400
订单数
1,204
—悬停:—<ElementSelectionOverlay target={root} showLabel={false} enabled={false} />何时用
要做「点预览里的元素 → 定位到源码 / 打开属性面板 / 喂给 prompt」这类指向编辑能力时用它。它只负责选中并给出路径,编辑面板由你自己搭。
- 引导用户看某个已知元素,用 Tour(它是遮罩镂空 + 气泡卡,目标由你指定,不做拾取)。
- 只想给元素加注解气泡,用 Annotation。
- 只想要一层不可交互的贴图覆盖,用 Watermark。
导入
import {
ElementSelectionOverlay,
asElement,
computeLabelPosition,
elementPath,
escapeAttributeValue,
findMarkedElement,
isRectVisible,
pathLabel,
resolveElementByPath,
structuralPath,
toHostRect,
} from "@hulianui/ui"Props
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| target * | HTMLElement | HTMLIFrameElement | null | — | 目标区域:普通容器(在其内部选择)或同源 iframe(在其文档内选择)。null 时不渲染也不监听 |
| enabled | boolean | true | 是否处于选择模式。false 停止拾取与点击拦截,已选中的框仍然保留 |
| highlightSelector | string | — | 可选中元素的选择器;落点会向上找最近的匹配祖先,匹配不到则不高亮(用来把粒度锁在组件级) |
| ignoreSelector | string | — | 排除选择器;命中(含祖先命中)的元素不可 hover / 选中 |
| showLabel | boolean | true | 是否显示标签。标签同一时刻只有一个,hover 优先于选中 |
| pathAttribute | string | "data-hulian-path" | 标记路径的属性名 |
| componentAttribute | string | "data-hulian-component" | 标记组件名的属性名(用于标签文案与 detail.component) |
| anchorOnId | boolean | true | 结构化路径遇到带 id 的祖先就锚定,不再上溯到根 |
| selectedPath | string | null | — | 受控选中路径。传了(含 null)即视为受控,组件不再自管选中态 |
| interceptClicks | boolean | true | 是否吞掉目标里的点击(阻止预览内的跳转 / 按钮触发) |
| zIndex | number | 100 | 叠加层 z-index |
| className | string | — | 叠加层容器类名 |
Events
| 事件 | 类型 | 说明 |
|---|---|---|
| onSelect | (path: string, detail: ElementSelectionDetail) => void | 选中:点击,或目标内按 Enter / 空格 |
| onHover | (path: string | null, detail: ElementSelectionDetail | null) => void | hover 变化;移出或落在不可选区域时给 (null, null) |
| onClear | () => void | 清除选中:点空白处 / 按 Esc |
| onError | (error: ElementSelectionOverlayError) => void | 目标不可接管(跨源 iframe 等)。同一目标只报一次。error.message 取自 ConfigProvider 的 locale(未包 Provider 时是内置中文),error.code 与语言无关,要分支判断请用 code |
ElementSelectionDetail = { path, source: "marked" | "structural", component, tagName, element, rect }。source 告诉你这条路径有多可靠:marked 读自标记属性(跨重排不失效),structural 是按 DOM 结构推的(DOM 一变可能失效)。
无障碍
- 叠加层
aria-hidden+pointer-events: none:不进无障碍树、不抢焦点、不挡目标交互。 - 不只靠颜色区分状态:hover 是虚线细框,选中是实线粗框,形态本身可分辨;标签再给出组件名/元素名。
- 键盘可达:目标内 Tab 到元素后按 Enter / 空格即选中,Esc 清除选中,不必依赖指针。
- 建议宿主再提供一条等价的键盘通路(如用 Tree 列出组件树),与
selectedPath双向绑定 —— 对屏幕阅读器用户来说,「在画布上找元素」始终不如「在列表里选节点」。 - 标签自动避让视口上边缘(贴顶时翻到框内侧),不会被裁掉。
禁忌 / 坑
- 跨源 iframe 不支持,而且不会静默失效:读不到
contentDocument时组件不渲染,并触发一次onError({ code: "cross-origin" })+ 开发期告警。要跨源就只有三条路:换成同源预览(srcDoc/ 同源代理域名)、或在被预览页里自己挂一份叠加层把 path 通过postMessage回传宿主、或放弃指向编辑。本组件不提供 postMessage 桥(那是另一套协议,不该藏在一个 UI 组件里)。 - 能打标记就打标记。
structural路径是按 DOM 结构推的,插一个兄弟节点、条件渲染一变就可能指到别处。把data-hulian-path打在组件根节点上,路径才跨重排稳定。 - iframe 内的元素跨 realm:它们不是宿主的
Element实例,node instanceof Element恒 false —— 自己处理目标事件时请用导出的asElement(),别用 instanceof(这是同源 iframe 场景最容易踩的静默失效)。 interceptClicks默认true,意味着选择模式下预览是不可交互的(点击被吞、mousedown 的默认行为被挡)。要让用户一边操作预览一边看高亮,传interceptClicks={false},或用enabled显式切换选择模式。- 目标根自身不可选:
target那个元素(iframe 场景是其body)hover 与点击都视为空白,点它触发onClear。根若也带标记属性,所有元素会退化成同一条路径,所以标记查找刻意跳过根。 - jsdom 下 `getBoundingClientRect` 恒为 0,零面积一律判不可见 → 单测里默认一个框都不会渲染。要断言框的存在,先给
Element.prototype.getBoundingClientRect打桩;但别断言坐标数值(坐标来自你的桩,等于自证)。坐标逻辑请测toHostRect/computeLabelPosition这两个纯函数。 - 目标是
document.body时,本组件的 portal 层就落在目标内部;组件已过滤掉自身引起的 MutationObserver 记录(否则自触发死循环),你若另外挂了 observer 也要做同样的过滤。 - 客户端组件(要读 DOM、要挂监听),必须在 client 上下文用;SSR 期不渲染任何东西。
相关
Tour · Annotation · Watermark · Flow · Tree
Playground
指向编辑预览区
把指针移进来,点任意一块即可选中。
本月收入
¥ 128,400
订单数
1,204
—悬停:—<ElementSelectionOverlay
target={previewRoot}
highlightSelector="[data-hulian-component]"
selectedPath={selected}
onSelect={(path, detail) => setSelected(path)}
/>