@greypan/web-ui 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,7 +38,66 @@ function c(e) {
38
38
  function l(e) {
39
39
  return [...e].filter((e) => e !== void 0).flatMap((e) => Array.from(e.querySelectorAll(t))).find((e) => !!e.shadowRoot?.activeElement);
40
40
  }
41
+ function u(e, t, n, r) {
42
+ let a = i(t), o = i(e), s = /* @__PURE__ */ new Set([...a, ...o]);
43
+ n.forEach((e, t) => {
44
+ s.has(t) || (e.remove(), n.delete(t));
45
+ });
46
+ let c = (t, i, a) => {
47
+ let o = n.get(t);
48
+ if (o && o.parentNode === e) return o;
49
+ o?.remove();
50
+ let s = document.createComment(r);
51
+ return i ? e.insertBefore(s, t) : a && a.parentNode === e ? e.insertBefore(s, a) : e.appendChild(s), n.set(t, s), s;
52
+ };
53
+ for (let e = a.length - 1; e >= 0; e--) {
54
+ let t = a[e], r = a[e + 1];
55
+ c(t, !1, r ? n.get(r) : void 0);
56
+ }
57
+ for (let e of o) c(e, !0);
58
+ for (let e of o) t.appendChild(e);
59
+ }
60
+ function d(e, t, n, r) {
61
+ let i = [];
62
+ for (let a of Array.from(e.childNodes)) a.nodeType === Node.COMMENT_NODE && a.textContent === r && n.forEach((e, n) => {
63
+ e === a && n.parentNode === t && i.push(n);
64
+ });
65
+ if (i.length === 0) return;
66
+ let a = Array.from(t.children);
67
+ if (!(a.length === i.length && a.every((e, t) => e === i[t]))) for (let e = i.length - 1; e >= 0; e--) {
68
+ let n = i[e], r = i[e + 1];
69
+ r ? n.nextElementSibling !== r && t.insertBefore(n, r) : t.appendChild(n);
70
+ }
71
+ }
72
+ function f(e, t) {
73
+ let n = /* @__PURE__ */ new Map();
74
+ for (let r of Array.from(e.childNodes)) if (r instanceof Comment && r.textContent !== t) {
75
+ let e = r.nextElementSibling;
76
+ n.set(r, e instanceof HTMLElement ? e : null);
77
+ }
78
+ return n;
79
+ }
80
+ function p(e, t) {
81
+ for (let [n, r] of t) n.parentNode === e && (r && r.parentNode === e ? n.nextElementSibling !== r && e.insertBefore(n, r) : e.appendChild(n));
82
+ }
83
+ function m(e, t, n) {
84
+ let r = [];
85
+ for (let i of Array.from(t.childNodes)) {
86
+ if (i instanceof Comment) {
87
+ r.push(i);
88
+ continue;
89
+ }
90
+ if (!(i instanceof HTMLElement)) continue;
91
+ let t = n.get(i);
92
+ if (t) {
93
+ for (let n of r) t.parentNode === e ? e.insertBefore(n, t) : e.appendChild(n);
94
+ r.length = 0, t.parentNode === e ? e.insertBefore(i, t) : e.appendChild(i), t.remove(), n.delete(i);
95
+ }
96
+ }
97
+ for (let t of r) e.appendChild(t);
98
+ n.forEach((e) => e.remove()), n.clear();
99
+ }
41
100
  //#endregion
42
- export { e as MENU_CONTENT_SELECTOR, t as MENU_ITEM_SELECTOR, l as findFocusedMenuItem, c as focusMenuItem, r as getEnabledMenuItems, n as getMenuChildren, s as getMenuItemFromEvent, i as getMovableMenuSubtrees, o as hideNestedMenuChildren, a as moveMenuChildren };
101
+ export { e as MENU_CONTENT_SELECTOR, t as MENU_ITEM_SELECTOR, f as captureFrameworkAnchors, l as findFocusedMenuItem, c as focusMenuItem, r as getEnabledMenuItems, n as getMenuChildren, s as getMenuItemFromEvent, i as getMovableMenuSubtrees, o as hideNestedMenuChildren, a as moveMenuChildren, d as orderManagedMenuItems, u as reconcileManagedMenuItems, p as restoreFrameworkAnchors, m as returnManagedMenuItemsToSlot };
43
102
 
44
103
  //# sourceMappingURL=menu-tree.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"menu-tree.js","names":[],"sources":["../../../src/shared/menu-portal/menu-tree.ts"],"sourcesContent":["export const MENU_CONTENT_SELECTOR = 'web-ui-dropdown-item, web-ui-dropdown-divider, web-ui-dropdown-header'\nexport const MENU_ITEM_SELECTOR = 'web-ui-dropdown-item'\n\nexport function getMenuChildren(container: ParentNode): HTMLElement[] {\n const children: HTMLElement[] = []\n for (const child of Array.from(container.children)) {\n if (!(child instanceof HTMLElement)) continue\n if (child.matches(MENU_CONTENT_SELECTOR)) {\n children.push(child)\n continue\n }\n\n // 框架条件渲染会用元素包裹自有节点,或留下注释锚点。搜索 wrapper,\n // 但不进入 menu item 内部;其子节点属于嵌套 submenu。\n children.push(...getMenuChildren(child))\n }\n return children\n}\n\nexport function getEnabledMenuItems(container: ParentNode): HTMLElement[] {\n return getMenuChildren(container).filter(item => item.matches(`${MENU_ITEM_SELECTOR}:not([disabled])`))\n}\n\nexport function getMovableMenuSubtrees(container: ParentNode): HTMLElement[] {\n const subtrees: HTMLElement[] = []\n for (const child of Array.from(container.children)) {\n if (!(child instanceof HTMLElement)) continue\n if (child.matches(MENU_CONTENT_SELECTOR) || getMenuChildren(child).length > 0) subtrees.push(child)\n }\n return subtrees\n}\n\nexport function moveMenuChildren(source: ParentNode, target: HTMLElement) {\n getMovableMenuSubtrees(source).forEach(subtree => {\n // 正常调用 source !== target,移走后项不再属于 source,天然收敛;\n // 这里兜底防御 source === target 的误用,避免 appendChild 自移动触发观察者刷新循环。\n if (subtree.parentNode === target) return\n target.appendChild(subtree)\n })\n}\n\nexport function hideNestedMenuChildren(root: ParentNode, slot: string) {\n root.querySelectorAll<HTMLElement>('web-ui-dropdown-item[submenu]').forEach(item => {\n getMenuChildren(item).forEach(child => child.setAttribute('slot', slot))\n })\n}\n\nexport function getMenuItemFromEvent(event: Event): HTMLElement | null {\n return (\n event\n .composedPath()\n .find((node): node is HTMLElement => node instanceof HTMLElement && node.matches(MENU_ITEM_SELECTOR)) ?? null\n )\n}\n\nexport function focusMenuItem(item: HTMLElement | undefined) {\n if (!item || item.hasAttribute('disabled')) return\n ;(item as HTMLElement & { focusItem?: () => void }).focusItem?.()\n}\n\nexport function findFocusedMenuItem(panels: Iterable<HTMLElement | undefined>): HTMLElement | undefined {\n return [...panels]\n .filter((panel): panel is HTMLElement => panel !== undefined)\n .flatMap(panel => Array.from(panel.querySelectorAll<HTMLElement>(MENU_ITEM_SELECTOR)))\n .find(item => Boolean(item.shadowRoot?.activeElement))\n}\n"],"mappings":";AAAA,IAAa,IAAwB,yEACxB,IAAqB;AAElC,SAAgB,EAAgB,GAAsC;CACpE,IAAM,IAA0B,CAAC;CACjC,KAAK,IAAM,KAAS,MAAM,KAAK,EAAU,QAAQ,GACzC,iBAAiB,aACvB;MAAI,EAAM,QAAA,uEAA6B,GAAG;GACxC,EAAS,KAAK,CAAK;GACnB;EACF;EAIA,EAAS,KAAK,GAAG,EAAgB,CAAK,CAAC;CAJvC;CAMF,OAAO;AACT;AAEA,SAAgB,EAAoB,GAAsC;CACxE,OAAO,EAAgB,CAAS,CAAC,CAAC,QAAO,MAAQ,EAAK,QAAQ,GAAG,EAAmB,iBAAiB,CAAC;AACxG;AAEA,SAAgB,EAAuB,GAAsC;CAC3E,IAAM,IAA0B,CAAC;CACjC,KAAK,IAAM,KAAS,MAAM,KAAK,EAAU,QAAQ,GACzC,aAAiB,gBACnB,EAAM,QAAA,uEAA6B,KAAK,EAAgB,CAAK,CAAC,CAAC,SAAS,MAAG,EAAS,KAAK,CAAK;CAEpG,OAAO;AACT;AAEA,SAAgB,EAAiB,GAAoB,GAAqB;CACxE,EAAuB,CAAM,CAAC,CAAC,SAAQ,MAAW;EAG5C,EAAQ,eAAe,KAC3B,EAAO,YAAY,CAAO;CAC5B,CAAC;AACH;AAEA,SAAgB,EAAuB,GAAkB,GAAc;CACrE,EAAK,iBAA8B,+BAA+B,CAAC,CAAC,SAAQ,MAAQ;EAClF,EAAgB,CAAI,CAAC,CAAC,SAAQ,MAAS,EAAM,aAAa,QAAQ,CAAI,CAAC;CACzE,CAAC;AACH;AAEA,SAAgB,EAAqB,GAAkC;CACrE,OACE,EACG,aAAa,CAAC,CACd,MAAM,MAA8B,aAAgB,eAAe,EAAK,QAAA,sBAA0B,CAAC,KAAK;AAE/G;AAEA,SAAgB,EAAc,GAA+B;CACvD,CAAC,KAAQ,EAAK,aAAa,UAAU,KACxC,EAAmD,YAAY;AAClE;AAEA,SAAgB,EAAoB,GAAoE;CACtG,OAAO,CAAC,GAAG,CAAM,CAAC,CACf,QAAQ,MAAgC,MAAU,KAAA,CAAS,CAAC,CAC5D,SAAQ,MAAS,MAAM,KAAK,EAAM,iBAA8B,CAAkB,CAAC,CAAC,CAAC,CACrF,MAAK,MAAQ,EAAQ,EAAK,YAAY,aAAc;AACzD"}
1
+ {"version":3,"file":"menu-tree.js","names":[],"sources":["../../../src/shared/menu-portal/menu-tree.ts"],"sourcesContent":["export const MENU_CONTENT_SELECTOR = 'web-ui-dropdown-item, web-ui-dropdown-divider, web-ui-dropdown-header'\nexport const MENU_ITEM_SELECTOR = 'web-ui-dropdown-item'\n\nexport function getMenuChildren(container: ParentNode): HTMLElement[] {\n const children: HTMLElement[] = []\n for (const child of Array.from(container.children)) {\n if (!(child instanceof HTMLElement)) continue\n if (child.matches(MENU_CONTENT_SELECTOR)) {\n children.push(child)\n continue\n }\n\n // 框架条件渲染会用元素包裹自有节点,或留下注释锚点。搜索 wrapper,\n // 但不进入 menu item 内部;其子节点属于嵌套 submenu。\n children.push(...getMenuChildren(child))\n }\n return children\n}\n\nexport function getEnabledMenuItems(container: ParentNode): HTMLElement[] {\n return getMenuChildren(container).filter(item => item.matches(`${MENU_ITEM_SELECTOR}:not([disabled])`))\n}\n\nexport function getMovableMenuSubtrees(container: ParentNode): HTMLElement[] {\n const subtrees: HTMLElement[] = []\n for (const child of Array.from(container.children)) {\n if (!(child instanceof HTMLElement)) continue\n if (child.matches(MENU_CONTENT_SELECTOR) || getMenuChildren(child).length > 0) subtrees.push(child)\n }\n return subtrees\n}\n\nexport function moveMenuChildren(source: ParentNode, target: HTMLElement) {\n getMovableMenuSubtrees(source).forEach(subtree => {\n // 正常调用 source !== target,移走后项不再属于 source,天然收敛;\n // 这里兜底防御 source === target 的误用,避免 appendChild 自移动触发观察者刷新循环。\n if (subtree.parentNode === target) return\n target.appendChild(subtree)\n })\n}\n\nexport function hideNestedMenuChildren(root: ParentNode, slot: string) {\n root.querySelectorAll<HTMLElement>('web-ui-dropdown-item[submenu]').forEach(item => {\n getMenuChildren(item).forEach(child => child.setAttribute('slot', slot))\n })\n}\n\nexport function getMenuItemFromEvent(event: Event): HTMLElement | null {\n return (\n event\n .composedPath()\n .find((node): node is HTMLElement => node instanceof HTMLElement && node.matches(MENU_ITEM_SELECTOR)) ?? null\n )\n}\n\nexport function focusMenuItem(item: HTMLElement | undefined) {\n if (!item || item.hasAttribute('disabled')) return\n ;(item as HTMLElement & { focusItem?: () => void }).focusItem?.()\n}\n\nexport function findFocusedMenuItem(panels: Iterable<HTMLElement | undefined>): HTMLElement | undefined {\n return [...panels]\n .filter((panel): panel is HTMLElement => panel !== undefined)\n .flatMap(panel => Array.from(panel.querySelectorAll<HTMLElement>(MENU_ITEM_SELECTOR)))\n .find(item => Boolean(item.shadowRoot?.activeElement))\n}\n\n/**\n * 托管菜单项的模板位锚:每个被迁入面板的项在宿主 light DOM 留一个 marker 注释,\n * 表示其模板位。宿主因此始终持有完整有序骨架(marker + 框架注释锚点 + 非菜单节点),\n * 框架(Vue/React 条件渲染)打开期的插入以宿主为容器、以真实锚点为基准;\n * 面板项的顺序按 marker 序重排;关闭时按 marker 位双向归还。\n * context-menu 与 dropdown 的打开期实时渲染共用本机制。\n */\nexport type MenuItemAnchors = Map<HTMLElement, Comment>\n\n/**\n * 全集 reconcile:以「content 现有元素 ∪ 宿主元素」为托管全集。\n * 框架(如 Vue 的 v-if 翻转)可能不经宿主直接改写 portal 内节点——卸载旧项留下\n * 注释锚点、把新项直接插进 content——它们都会被这里收编(补 marker + 补隐藏由调用方负责),\n * 已被框架删除/移出两处的条目则清理 marker。最后把宿主中尚存的托管项移入 content。\n */\nexport function reconcileManagedMenuItems(\n host: HTMLElement,\n content: HTMLElement,\n anchors: MenuItemAnchors,\n markerText: string\n): void {\n // 宿主与 portal 面板分别位于不同容器,结构上不相交且收集只看直接子层,\n // 两个集合不会重复。\n const inContent = getMovableMenuSubtrees(content)\n const inHost = getMovableMenuSubtrees(host)\n const managed = new Set([...inContent, ...inHost])\n\n // 清理已失效条目:托管元素既不在 content 也不在宿主,说明已被框架删除。\n // 仍在 content 的元素必被上面收集进 managed,故这里只需处理宿主侧。\n anchors.forEach((marker, subtree) => {\n if (managed.has(subtree)) return\n marker.remove()\n anchors.delete(subtree)\n })\n\n // 确保每个托管元素都有 marker:框架直接插入 content 的项在此收编。\n // content 项按其当前 DOM 序(框架语义序)**反向**处理,新 marker 插到后继项 marker 之前——\n // 这样补建的 marker 序无需重排即可与 content 对齐;宿主项的 marker 原地补插。\n const ensureMarker = (subtree: HTMLElement, inHost: boolean, before?: Comment) => {\n const existing = anchors.get(subtree)\n if (existing && existing.parentNode === host) return existing\n existing?.remove()\n const marker = document.createComment(markerText)\n if (inHost) {\n host.insertBefore(marker, subtree)\n } else if (before && before.parentNode === host) {\n host.insertBefore(marker, before)\n } else {\n host.appendChild(marker)\n }\n anchors.set(subtree, marker)\n return marker\n }\n for (let index = inContent.length - 1; index >= 0; index--) {\n const subtree = inContent[index]\n const next = inContent[index + 1]\n const nextMarker = next ? anchors.get(next) : undefined\n ensureMarker(subtree, false, nextMarker)\n }\n for (const subtree of inHost) {\n ensureMarker(subtree, true)\n }\n\n // 宿主中尚存的托管项移入 content。\n for (const subtree of inHost) {\n content.appendChild(subtree)\n }\n}\n\n/** 按宿主 marker 序重排 content 项;顺序已一致时早退。 */\nexport function orderManagedMenuItems(\n host: HTMLElement,\n content: HTMLElement,\n anchors: MenuItemAnchors,\n markerText: string\n): void {\n const targetItems: HTMLElement[] = []\n for (const node of Array.from(host.childNodes)) {\n if (node.nodeType !== Node.COMMENT_NODE || node.textContent !== markerText) continue\n anchors.forEach((marker, subtree) => {\n if (marker !== node || subtree.parentNode !== content) return\n targetItems.push(subtree)\n })\n }\n\n if (targetItems.length === 0) return\n\n const currentItems = Array.from(content.children)\n if (currentItems.length === targetItems.length && currentItems.every((item, index) => item === targetItems[index]))\n return\n\n // 把每个元素移动到目标序中下一个元素之前,而不是 appendChild 全部移到末尾。\n // appendChild 会把元素越过 content 中的框架注释锚点(v-if 锚点),导致锚点脱离\n // 模板位置,下次框架翻转时据此锚点插入新分支项就会落错位;insertBefore 只重排\n // 元素相对顺序,锚点保持在原模板位置。锚点复位由 restoreFrameworkAnchors 独立完成。\n for (let index = targetItems.length - 1; index >= 0; index--) {\n const item = targetItems[index]\n const next = targetItems[index + 1]\n if (next) {\n if (item.nextElementSibling !== next) content.insertBefore(item, next)\n } else {\n content.appendChild(item)\n }\n }\n}\n\n/**\n * 捕获框架 v-if 注释锚点相对其后继元素的绑定,供重排后复位。\n * 后继以 nextElementSibling 解析(跳过注释/文本,多个相邻锚点绑定同一后继)。\n * 锚点在 content 末尾、无后继元素时记为 null(尾部锚点,复位时保持末尾)。\n */\nexport function captureFrameworkAnchors(content: HTMLElement, markerText: string): Map<Comment, HTMLElement | null> {\n const anchors = new Map<Comment, HTMLElement | null>()\n for (const node of Array.from(content.childNodes)) {\n if (node instanceof Comment && node.textContent !== markerText) {\n const nextEl = node.nextElementSibling\n anchors.set(node, nextEl instanceof HTMLElement ? nextEl : null)\n }\n }\n return anchors\n}\n\n/**\n * 把锚点复位到捕获时其后继元素之前(尾部锚点保持末尾)。\n * 元素重排用 insertBefore/appendChild 不会移动注释,锚点必须显式复位,\n * 否则脱离模板位置后 Vue 下次 v-if 翻转会把新分支项插到错误插入点。\n */\nexport function restoreFrameworkAnchors(content: HTMLElement, anchors: Map<Comment, HTMLElement | null>): void {\n for (const [anchor, successor] of anchors) {\n if (anchor.parentNode !== content) continue\n if (successor && successor.parentNode === content) {\n if (anchor.nextElementSibling !== successor) content.insertBefore(anchor, successor)\n } else {\n // 尾部锚点:重排可能把元素 append 到末尾越过锚点,恢复其末尾位置。\n content.appendChild(anchor)\n }\n }\n}\n\n/**\n * 关闭恢复:按 content 子节点序双向归还——元素回到宿主中自己的 marker 位,\n * 框架 v-if 注释锚点插到对应元素之前。Vue 仍持有这些锚点的 vnode.el 引用,\n * 随元素一起迁回宿主可确保框架下次 patch 以宿主为容器,不会因 parentNode === null 崩溃。\n * 归还后清除全部 marker 与登记。\n */\nexport function returnManagedMenuItemsToSlot(host: HTMLElement, content: HTMLElement, anchors: MenuItemAnchors): void {\n const pending: Comment[] = []\n for (const node of Array.from(content.childNodes)) {\n // instanceof 收窄:Comment 接口为空,nodeType 继承自 Node 的 number,无法用 === 收窄\n if (node instanceof Comment) {\n pending.push(node)\n continue\n }\n if (!(node instanceof HTMLElement)) continue\n const marker = anchors.get(node)\n if (!marker) continue\n for (const c of pending) {\n if (marker.parentNode === host) host.insertBefore(c, marker)\n else host.appendChild(c)\n }\n pending.length = 0\n if (marker.parentNode === host) {\n host.insertBefore(node, marker)\n } else {\n host.appendChild(node)\n }\n marker.remove()\n anchors.delete(node)\n }\n // 尾部框架锚点追加到宿主末尾\n for (const c of pending) host.appendChild(c)\n // 映射中剩余 = 已不在 content 的条目(如翻转中被框架卸载的),仅清理 marker\n anchors.forEach(marker => marker.remove())\n anchors.clear()\n}\n"],"mappings":"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"}
@@ -41,7 +41,7 @@ var t = "web-ui-option", n = () => e((e) => {
41
41
  if (!t || !r) return;
42
42
  for (let i of n) !r.contains(i) && !e.element.contains(i) && t.removeContent([i]);
43
43
  if (n.clear(), !e.isOpen()) return;
44
- let i = e.getMigratableNodes();
44
+ let i = e.getMigratableNodes().filter((e) => !(e instanceof Comment));
45
45
  i.length && t.appendContent(i, r);
46
46
  },
47
47
  scheduleRefresh() {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/shared/option-portal/index.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\n\nimport type { WebUiOption } from '@/components/option'\nimport type { OverlayPortal } from '@/shared/overlay/portal'\n\n/**\n * select / autocomplete 共用的 portal option 域:宿主侧 option 注册表、\n * portal 内容同步(打开期迁移 + 真删除清理)与微任务调度。\n *\n * 交互语义与 Lit 响应式由各组件保留(同 anchored-panel 的边界约定):\n * 组件通过 host 回调注入差异点(迁移节点来源、打开期即时同步等),\n * 并持有 _options/_activeIndex 的 @state,shared 只经返回值同步数据。\n */\n\nexport interface OptionPortalHost {\n /** 承载宿主级监听的元素(select / autocomplete 宿主自身)。 */\n readonly element: HTMLElement\n /** option id 前缀;宿主应保证跨实例唯一(如含实例序号),供 aria 引用与样式定位。 */\n readonly idPrefix: string\n getPortal(): OverlayPortal | undefined\n getPortalContent(): HTMLElement | undefined\n /** 打开判定:syncPortalContent 仅在打开期执行内容迁移。 */\n isOpen(): boolean\n /**\n * 打开期待迁移的节点来源。两组件粒度不同(select 迁裸 option,\n * autocomplete 迁全部 childNodes),保持各自现状不在此统一。\n */\n getMigratableNodes(): Node[]\n /** 微任务回调中判断宿主是否已完成首次渲染(Lit hasUpdated)。 */\n hasUpdated(): boolean\n /** 微任务末的宿主刷新(requestUpdate)。 */\n requestUpdate(): void\n /**\n * 打开期即时同步钩子。select 在微任务内立即刷新标签与激活态\n * (portal 打开时 willUpdate 查不到面板外的新增项),autocomplete 无此需要。\n */\n onImmediateSync?(): void\n /**\n * 组件侧的逐元素交互监听(click/pointerover/pointerdown/option-update)。\n * shared 负责随 refresh 配对调用,语义由组件定义。\n */\n bindOption(option: WebUiOption): void\n unbindOption(option: WebUiOption): void\n}\n\nexport interface OptionPortalApi {\n /** 面板优先查询,面板无 option 或关闭时回退 light DOM。 */\n queryOptions(): WebUiOption[]\n /**\n * diff 出旧集合解绑、新集合绑定;按 option 身份保持激活索引。\n * 返回新注册表数组与新的激活索引(-1 表示激活项已失效,如移除/迁移)。\n */\n refresh(\n previous: readonly WebUiOption[],\n activeOption: WebUiOption | undefined\n ): {\n options: WebUiOption[]\n activeIndex: number\n }\n /** 稳定 id 分配(WeakMap 身份 + 冲突循环),跨 remove/reorder 保持不变。 */\n ensureOptionIds(options: readonly WebUiOption[]): void\n /** pending 真删除清理 + 打开期把滞留 light DOM 的节点迁入面板。 */\n syncPortalContent(): void\n /** register/unregister/contentChange 的统一微任务收敛入口(slot 监听由组件模板持有)。 */\n scheduleRefresh(): void\n /** 宿主级与逐元素 unregister 监听共用的处理器。 */\n handleOptionUnregister(event: Event): void\n /** 宿主级监听配对(connectedCallback 调用,与 dispose 配对)。 */\n bindHost(): void\n /** 断连清理:解绑宿主监听并清空 pending 集合。 */\n dispose(): void\n}\n\nconst OPTION_TAG = 'web-ui-option'\n\nexport const defineOptionPortal = () =>\n definePlugin<OptionPortalApi, OptionPortalHost>(host => {\n const pendingUnregistered = new Set<WebUiOption>()\n // 稳定 id:WeakMap 按 option 身份记忆分配结果,remove/reorder 后 id 不变;\n // aria-activedescendant / 外部样式都依赖 id 的时序稳定性。\n const optionIds = new WeakMap<WebUiOption, string>()\n let nextOptionId = 0\n\n const bindOption = (option: WebUiOption) => {\n host.bindOption(option)\n option.addEventListener('option-unregister', handleOptionUnregister)\n }\n\n const unbindOption = (option: WebUiOption) => {\n host.unbindOption(option)\n option.removeEventListener('option-unregister', handleOptionUnregister)\n }\n\n const handleOptionUnregister = (event: Event) => {\n // Portal 迁移与真实删除都会触发 unregister;先记录候选,\n // 微任务末 syncPortalContent 按最终位置区分二者。\n if (event.target instanceof HTMLElement && event.target.localName === OPTION_TAG) {\n pendingUnregistered.add(event.target as WebUiOption)\n }\n api.scheduleRefresh()\n }\n\n const handleRegister = () => {\n api.scheduleRefresh()\n }\n\n const api: OptionPortalApi = {\n queryOptions() {\n const panel = host.getPortal()?.panel ?? host.getPortalContent()\n const panelOptions = panel ? [...panel.querySelectorAll<WebUiOption>(OPTION_TAG)] : []\n // 面板内容仅在浮层打开、moveContent 之后存在;关闭时回退 light DOM,\n // 否则 portal 组件首次加载时注册表为空。\n return panelOptions.length > 0 ? panelOptions : [...host.element.querySelectorAll<WebUiOption>(OPTION_TAG)]\n },\n\n refresh(previous, activeOption) {\n const nextOptions = api.queryOptions()\n\n // register/unregister 是 composed 事件:Portal 迁移时 target 会 retarget 成宿主,\n // 不能作为监听器绑定依据;绑定只发生在按 DOM 查询确认身份的 option 上。\n for (const option of previous) if (!nextOptions.includes(option)) unbindOption(option)\n nextOptions.forEach(bindOption)\n\n // 激活索引按 option 身份保持,避免移除中间项后静默偏移到相邻项\n return {\n options: nextOptions,\n activeIndex: activeOption && nextOptions.includes(activeOption) ? nextOptions.indexOf(activeOption) : -1\n }\n },\n\n ensureOptionIds(registryOptions) {\n const usedIds = new Set(registryOptions.map(option => option.id).filter((id): id is string => Boolean(id)))\n registryOptions.forEach(option => {\n if (option.id) return\n\n let id = optionIds.get(option)\n if (!id || usedIds.has(id)) {\n do {\n id = `${host.idPrefix}-option-${++nextOptionId}`\n } while (usedIds.has(id))\n optionIds.set(option, id)\n }\n option.id = id\n usedIds.add(id)\n })\n },\n\n syncPortalContent() {\n const portal = host.getPortal()\n const portalContent = host.getPortalContent()\n if (!portal || !portalContent) return\n\n // 真删除(既不在面板也不在 light DOM)的 option 从 portal 追踪列表摘除,\n // 否则关闭时 restoreContent 会把它复活回 light DOM。\n for (const option of pendingUnregistered) {\n if (!portalContent.contains(option) && !host.element.contains(option)) portal.removeContent([option])\n }\n pendingUnregistered.clear()\n\n if (!host.isOpen()) return\n\n // Portal 打开期间框架条件渲染(v-if)可能在 light DOM 插入新节点;\n // 面板内已有内容时 queryOptions 不会回退 light DOM,必须显式迁入面板。\n const nodes = host.getMigratableNodes()\n if (nodes.length) portal.appendContent(nodes, portalContent)\n },\n\n scheduleRefresh() {\n queueMicrotask(() => {\n if (!host.element.isConnected) return\n api.syncPortalContent()\n host.requestUpdate()\n // 关闭状态下 willUpdate 查不到 portal 面板内容;打开期的新增/删除\n // 由宿主在微任务内立即同步标签与激活态。\n if (host.isOpen() && host.hasUpdated()) host.onImmediateSync?.()\n })\n },\n\n handleOptionUnregister,\n\n bindHost() {\n host.element.addEventListener('option-register', handleRegister)\n host.element.addEventListener('option-unregister', handleOptionUnregister)\n },\n\n dispose() {\n host.element.removeEventListener('option-register', handleRegister)\n host.element.removeEventListener('option-unregister', handleOptionUnregister)\n pendingUnregistered.clear()\n }\n }\n\n return api\n })\n"],"mappings":";;AAyEA,IAAM,IAAa,iBAEN,UACX,GAAgD,MAAQ;CACtD,IAAM,oBAAsB,IAAI,IAAiB,GAG3C,oBAAY,IAAI,QAA6B,GAC/C,IAAe,GAEb,KAAc,MAAwB;EAE1C,AADA,EAAK,WAAW,CAAM,GACtB,EAAO,iBAAiB,qBAAqB,CAAsB;CACrE,GAEM,KAAgB,MAAwB;EAE5C,AADA,EAAK,aAAa,CAAM,GACxB,EAAO,oBAAoB,qBAAqB,CAAsB;CACxE,GAEM,KAA0B,MAAiB;EAM/C,AAHI,EAAM,kBAAkB,eAAe,EAAM,OAAO,cAAc,KACpE,EAAoB,IAAI,EAAM,MAAqB,GAErD,EAAI,gBAAgB;CACtB,GAEM,UAAuB;EAC3B,EAAI,gBAAgB;CACtB,GAEM,IAAuB;EAC3B,eAAe;GACb,IAAM,IAAQ,EAAK,UAAU,CAAC,EAAE,SAAS,EAAK,iBAAiB,GACzD,IAAe,IAAQ,CAAC,GAAG,EAAM,iBAA8B,CAAU,CAAC,IAAI,CAAC;GAGrF,OAAO,EAAa,SAAS,IAAI,IAAe,CAAC,GAAG,EAAK,QAAQ,iBAA8B,CAAU,CAAC;EAC5G;EAEA,QAAQ,GAAU,GAAc;GAC9B,IAAM,IAAc,EAAI,aAAa;GAIrC,KAAK,IAAM,KAAU,GAAU,AAAK,EAAY,SAAS,CAAM,KAAG,EAAa,CAAM;GAIrF,OAHA,EAAY,QAAQ,CAAU,GAGvB;IACL,SAAS;IACT,aAAa,KAAgB,EAAY,SAAS,CAAY,IAAI,EAAY,QAAQ,CAAY,IAAI;GACxG;EACF;EAEA,gBAAgB,GAAiB;GAC/B,IAAM,IAAU,IAAI,IAAI,EAAgB,KAAI,MAAU,EAAO,EAAE,CAAC,CAAC,QAAQ,MAAqB,EAAQ,CAAG,CAAC;GAC1G,EAAgB,SAAQ,MAAU;IAChC,IAAI,EAAO,IAAI;IAEf,IAAI,IAAK,EAAU,IAAI,CAAM;IAC7B,IAAI,CAAC,KAAM,EAAQ,IAAI,CAAE,GAAG;KAC1B;MACE,IAAK,GAAG,EAAK,SAAS,UAAU,EAAE;YAC3B,EAAQ,IAAI,CAAE;KACvB,EAAU,IAAI,GAAQ,CAAE;IAC1B;IAEA,AADA,EAAO,KAAK,GACZ,EAAQ,IAAI,CAAE;GAChB,CAAC;EACH;EAEA,oBAAoB;GAClB,IAAM,IAAS,EAAK,UAAU,GACxB,IAAgB,EAAK,iBAAiB;GAC5C,IAAI,CAAC,KAAU,CAAC,GAAe;GAI/B,KAAK,IAAM,KAAU,GACnB,AAAI,CAAC,EAAc,SAAS,CAAM,KAAK,CAAC,EAAK,QAAQ,SAAS,CAAM,KAAG,EAAO,cAAc,CAAC,CAAM,CAAC;GAItG,IAFA,EAAoB,MAAM,GAEtB,CAAC,EAAK,OAAO,GAAG;GAIpB,IAAM,IAAQ,EAAK,mBAAmB;GACtC,AAAI,EAAM,UAAQ,EAAO,cAAc,GAAO,CAAa;EAC7D;EAEA,kBAAkB;GAChB,qBAAqB;IACd,EAAK,QAAQ,gBAClB,EAAI,kBAAkB,GACtB,EAAK,cAAc,GAGf,EAAK,OAAO,KAAK,EAAK,WAAW,KAAG,EAAK,kBAAkB;GACjE,CAAC;EACH;EAEA;EAEA,WAAW;GAET,AADA,EAAK,QAAQ,iBAAiB,mBAAmB,CAAc,GAC/D,EAAK,QAAQ,iBAAiB,qBAAqB,CAAsB;EAC3E;EAEA,UAAU;GAGR,AAFA,EAAK,QAAQ,oBAAoB,mBAAmB,CAAc,GAClE,EAAK,QAAQ,oBAAoB,qBAAqB,CAAsB,GAC5E,EAAoB,MAAM;EAC5B;CACF;CAEA,OAAO;AACT,CAAC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/shared/option-portal/index.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\n\nimport type { WebUiOption } from '@/components/option'\nimport type { OverlayPortal } from '@/shared/overlay/portal'\n\n/**\n * select / autocomplete 共用的 portal option 域:宿主侧 option 注册表、\n * portal 内容同步(打开期迁移 + 真删除清理)与微任务调度。\n *\n * 交互语义与 Lit 响应式由各组件保留(同 anchored-panel 的边界约定):\n * 组件通过 host 回调注入差异点(迁移节点来源、打开期即时同步等),\n * 并持有 _options/_activeIndex 的 @state,shared 只经返回值同步数据。\n */\n\nexport interface OptionPortalHost {\n /** 承载宿主级监听的元素(select / autocomplete 宿主自身)。 */\n readonly element: HTMLElement\n /** option id 前缀;宿主应保证跨实例唯一(如含实例序号),供 aria 引用与样式定位。 */\n readonly idPrefix: string\n getPortal(): OverlayPortal | undefined\n getPortalContent(): HTMLElement | undefined\n /** 打开判定:syncPortalContent 仅在打开期执行内容迁移。 */\n isOpen(): boolean\n /**\n * 打开期待迁移的节点来源。两组件粒度不同(select 迁裸 option,\n * autocomplete 迁全部 childNodes),保持各自现状不在此统一。\n */\n getMigratableNodes(): Node[]\n /** 微任务回调中判断宿主是否已完成首次渲染(Lit hasUpdated)。 */\n hasUpdated(): boolean\n /** 微任务末的宿主刷新(requestUpdate)。 */\n requestUpdate(): void\n /**\n * 打开期即时同步钩子。select 在微任务内立即刷新标签与激活态\n * (portal 打开时 willUpdate 查不到面板外的新增项),autocomplete 无此需要。\n */\n onImmediateSync?(): void\n /**\n * 组件侧的逐元素交互监听(click/pointerover/pointerdown/option-update)。\n * shared 负责随 refresh 配对调用,语义由组件定义。\n */\n bindOption(option: WebUiOption): void\n unbindOption(option: WebUiOption): void\n}\n\nexport interface OptionPortalApi {\n /** 面板优先查询,面板无 option 或关闭时回退 light DOM。 */\n queryOptions(): WebUiOption[]\n /**\n * diff 出旧集合解绑、新集合绑定;按 option 身份保持激活索引。\n * 返回新注册表数组与新的激活索引(-1 表示激活项已失效,如移除/迁移)。\n */\n refresh(\n previous: readonly WebUiOption[],\n activeOption: WebUiOption | undefined\n ): {\n options: WebUiOption[]\n activeIndex: number\n }\n /** 稳定 id 分配(WeakMap 身份 + 冲突循环),跨 remove/reorder 保持不变。 */\n ensureOptionIds(options: readonly WebUiOption[]): void\n /** pending 真删除清理 + 打开期把滞留 light DOM 的节点迁入面板。 */\n syncPortalContent(): void\n /** register/unregister/contentChange 的统一微任务收敛入口(slot 监听由组件模板持有)。 */\n scheduleRefresh(): void\n /** 宿主级与逐元素 unregister 监听共用的处理器。 */\n handleOptionUnregister(event: Event): void\n /** 宿主级监听配对(connectedCallback 调用,与 dispose 配对)。 */\n bindHost(): void\n /** 断连清理:解绑宿主监听并清空 pending 集合。 */\n dispose(): void\n}\n\nconst OPTION_TAG = 'web-ui-option'\n\nexport const defineOptionPortal = () =>\n definePlugin<OptionPortalApi, OptionPortalHost>(host => {\n const pendingUnregistered = new Set<WebUiOption>()\n // 稳定 id:WeakMap 按 option 身份记忆分配结果,remove/reorder 后 id 不变;\n // aria-activedescendant / 外部样式都依赖 id 的时序稳定性。\n const optionIds = new WeakMap<WebUiOption, string>()\n let nextOptionId = 0\n\n const bindOption = (option: WebUiOption) => {\n host.bindOption(option)\n option.addEventListener('option-unregister', handleOptionUnregister)\n }\n\n const unbindOption = (option: WebUiOption) => {\n host.unbindOption(option)\n option.removeEventListener('option-unregister', handleOptionUnregister)\n }\n\n const handleOptionUnregister = (event: Event) => {\n // Portal 迁移与真实删除都会触发 unregister;先记录候选,\n // 微任务末 syncPortalContent 按最终位置区分二者。\n if (event.target instanceof HTMLElement && event.target.localName === OPTION_TAG) {\n pendingUnregistered.add(event.target as WebUiOption)\n }\n api.scheduleRefresh()\n }\n\n const handleRegister = () => {\n api.scheduleRefresh()\n }\n\n const api: OptionPortalApi = {\n queryOptions() {\n const panel = host.getPortal()?.panel ?? host.getPortalContent()\n const panelOptions = panel ? [...panel.querySelectorAll<WebUiOption>(OPTION_TAG)] : []\n // 面板内容仅在浮层打开、moveContent 之后存在;关闭时回退 light DOM,\n // 否则 portal 组件首次加载时注册表为空。\n return panelOptions.length > 0 ? panelOptions : [...host.element.querySelectorAll<WebUiOption>(OPTION_TAG)]\n },\n\n refresh(previous, activeOption) {\n const nextOptions = api.queryOptions()\n\n // register/unregister 是 composed 事件:Portal 迁移时 target 会 retarget 成宿主,\n // 不能作为监听器绑定依据;绑定只发生在按 DOM 查询确认身份的 option 上。\n for (const option of previous) if (!nextOptions.includes(option)) unbindOption(option)\n nextOptions.forEach(bindOption)\n\n // 激活索引按 option 身份保持,避免移除中间项后静默偏移到相邻项\n return {\n options: nextOptions,\n activeIndex: activeOption && nextOptions.includes(activeOption) ? nextOptions.indexOf(activeOption) : -1\n }\n },\n\n ensureOptionIds(registryOptions) {\n const usedIds = new Set(registryOptions.map(option => option.id).filter((id): id is string => Boolean(id)))\n registryOptions.forEach(option => {\n if (option.id) return\n\n let id = optionIds.get(option)\n if (!id || usedIds.has(id)) {\n do {\n id = `${host.idPrefix}-option-${++nextOptionId}`\n } while (usedIds.has(id))\n optionIds.set(option, id)\n }\n option.id = id\n usedIds.add(id)\n })\n },\n\n syncPortalContent() {\n const portal = host.getPortal()\n const portalContent = host.getPortalContent()\n if (!portal || !portalContent) return\n\n // 真删除(既不在面板也不在 light DOM)的 option 从 portal 追踪列表摘除,\n // 否则关闭时 restoreContent 会把它复活回 light DOM。\n for (const option of pendingUnregistered) {\n if (!portalContent.contains(option) && !host.element.contains(option)) portal.removeContent([option])\n }\n pendingUnregistered.clear()\n\n if (!host.isOpen()) return\n\n // Portal 打开期间框架条件渲染(v-if)可能在 light DOM 插入新节点;\n // 面板内已有内容时 queryOptions 不会回退 light DOM,必须显式迁入面板。\n // 注释(框架锚点与 portal 的 marker)不迁移:锚点留守宿主是实时渲染契约。\n const nodes = host.getMigratableNodes().filter(node => !(node instanceof Comment))\n if (nodes.length) portal.appendContent(nodes, portalContent)\n },\n\n scheduleRefresh() {\n queueMicrotask(() => {\n if (!host.element.isConnected) return\n api.syncPortalContent()\n host.requestUpdate()\n // 关闭状态下 willUpdate 查不到 portal 面板内容;打开期的新增/删除\n // 由宿主在微任务内立即同步标签与激活态。\n if (host.isOpen() && host.hasUpdated()) host.onImmediateSync?.()\n })\n },\n\n handleOptionUnregister,\n\n bindHost() {\n host.element.addEventListener('option-register', handleRegister)\n host.element.addEventListener('option-unregister', handleOptionUnregister)\n },\n\n dispose() {\n host.element.removeEventListener('option-register', handleRegister)\n host.element.removeEventListener('option-unregister', handleOptionUnregister)\n pendingUnregistered.clear()\n }\n }\n\n return api\n })\n"],"mappings":";;AAyEA,IAAM,IAAa,iBAEN,UACX,GAAgD,MAAQ;CACtD,IAAM,oBAAsB,IAAI,IAAiB,GAG3C,oBAAY,IAAI,QAA6B,GAC/C,IAAe,GAEb,KAAc,MAAwB;EAE1C,AADA,EAAK,WAAW,CAAM,GACtB,EAAO,iBAAiB,qBAAqB,CAAsB;CACrE,GAEM,KAAgB,MAAwB;EAE5C,AADA,EAAK,aAAa,CAAM,GACxB,EAAO,oBAAoB,qBAAqB,CAAsB;CACxE,GAEM,KAA0B,MAAiB;EAM/C,AAHI,EAAM,kBAAkB,eAAe,EAAM,OAAO,cAAc,KACpE,EAAoB,IAAI,EAAM,MAAqB,GAErD,EAAI,gBAAgB;CACtB,GAEM,UAAuB;EAC3B,EAAI,gBAAgB;CACtB,GAEM,IAAuB;EAC3B,eAAe;GACb,IAAM,IAAQ,EAAK,UAAU,CAAC,EAAE,SAAS,EAAK,iBAAiB,GACzD,IAAe,IAAQ,CAAC,GAAG,EAAM,iBAA8B,CAAU,CAAC,IAAI,CAAC;GAGrF,OAAO,EAAa,SAAS,IAAI,IAAe,CAAC,GAAG,EAAK,QAAQ,iBAA8B,CAAU,CAAC;EAC5G;EAEA,QAAQ,GAAU,GAAc;GAC9B,IAAM,IAAc,EAAI,aAAa;GAIrC,KAAK,IAAM,KAAU,GAAU,AAAK,EAAY,SAAS,CAAM,KAAG,EAAa,CAAM;GAIrF,OAHA,EAAY,QAAQ,CAAU,GAGvB;IACL,SAAS;IACT,aAAa,KAAgB,EAAY,SAAS,CAAY,IAAI,EAAY,QAAQ,CAAY,IAAI;GACxG;EACF;EAEA,gBAAgB,GAAiB;GAC/B,IAAM,IAAU,IAAI,IAAI,EAAgB,KAAI,MAAU,EAAO,EAAE,CAAC,CAAC,QAAQ,MAAqB,EAAQ,CAAG,CAAC;GAC1G,EAAgB,SAAQ,MAAU;IAChC,IAAI,EAAO,IAAI;IAEf,IAAI,IAAK,EAAU,IAAI,CAAM;IAC7B,IAAI,CAAC,KAAM,EAAQ,IAAI,CAAE,GAAG;KAC1B;MACE,IAAK,GAAG,EAAK,SAAS,UAAU,EAAE;YAC3B,EAAQ,IAAI,CAAE;KACvB,EAAU,IAAI,GAAQ,CAAE;IAC1B;IAEA,AADA,EAAO,KAAK,GACZ,EAAQ,IAAI,CAAE;GAChB,CAAC;EACH;EAEA,oBAAoB;GAClB,IAAM,IAAS,EAAK,UAAU,GACxB,IAAgB,EAAK,iBAAiB;GAC5C,IAAI,CAAC,KAAU,CAAC,GAAe;GAI/B,KAAK,IAAM,KAAU,GACnB,AAAI,CAAC,EAAc,SAAS,CAAM,KAAK,CAAC,EAAK,QAAQ,SAAS,CAAM,KAAG,EAAO,cAAc,CAAC,CAAM,CAAC;GAItG,IAFA,EAAoB,MAAM,GAEtB,CAAC,EAAK,OAAO,GAAG;GAKpB,IAAM,IAAQ,EAAK,mBAAmB,CAAC,CAAC,QAAO,MAAQ,EAAE,aAAgB,QAAQ;GACjF,AAAI,EAAM,UAAQ,EAAO,cAAc,GAAO,CAAa;EAC7D;EAEA,kBAAkB;GAChB,qBAAqB;IACd,EAAK,QAAQ,gBAClB,EAAI,kBAAkB,GACtB,EAAK,cAAc,GAGf,EAAK,OAAO,KAAK,EAAK,WAAW,KAAG,EAAK,kBAAkB;GACjE,CAAC;EACH;EAEA;EAEA,WAAW;GAET,AADA,EAAK,QAAQ,iBAAiB,mBAAmB,CAAc,GAC/D,EAAK,QAAQ,iBAAiB,qBAAqB,CAAsB;EAC3E;EAEA,UAAU;GAGR,AAFA,EAAK,QAAQ,oBAAoB,mBAAmB,CAAc,GAClE,EAAK,QAAQ,oBAAoB,qBAAqB,CAAsB,GAC5E,EAAoB,MAAM;EAC5B;CACF;CAEA,OAAO;AACT,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"overlay.js","names":[],"sources":["../../../src/shared/overlay/overlay.ts"],"sourcesContent":["import {\n autoUpdate,\n computePosition,\n flip,\n offset,\n shift,\n size,\n type Middleware,\n type Placement,\n type Strategy\n} from '@floating-ui/dom'\nimport { definePlugin } from '@greypan/js-kit'\n\nexport interface OverlayOptions {\n placement?: Placement\n offset?: number\n flip?: boolean\n shift?: boolean\n /** 面板宽度与锚点宽度保持一致。 */\n matchWidth?: boolean\n /** 面板至少与锚点同宽,同时允许内容将面板撑宽。 */\n minAnchorWidth?: boolean\n strategy?: Strategy\n}\n\n/** 虚拟锚点:无对应 DOM 元素的定位基准(如右键菜单的鼠标坐标)。 */\nexport interface OverlayVirtualAnchor {\n getBoundingClientRect(): DOMRect\n}\n\nexport type OverlayAnchor = HTMLElement | OverlayVirtualAnchor\n\nexport interface OverlayApi {\n isOpen(): boolean\n readonly options: Required<OverlayOptions>\n open(): void\n close(): void\n toggle(): void\n update(options: Partial<OverlayOptions>): void\n updateAnchor(anchor: OverlayAnchor): void\n dispose(): void\n}\n\nconst DEFAULT_OVERLAY_MIN_WIDTH = 120\nconst OVERLAY_MIN_WIDTH_VARIABLE = '--wui-overlay-min-width'\n\nconst DEFAULT_OPTIONS: Required<OverlayOptions> = {\n placement: 'bottom-start',\n offset: 4,\n flip: true,\n shift: true,\n matchWidth: false,\n minAnchorWidth: false,\n strategy: 'absolute'\n}\n\ninterface WidthStyleState {\n hasManagedStyles: boolean\n}\n\nfunction readOverlayMinWidth(overlay: HTMLElement): number {\n if (typeof getComputedStyle !== 'function') return DEFAULT_OVERLAY_MIN_WIDTH\n\n const minWidth = Number.parseFloat(getComputedStyle(overlay).getPropertyValue(OVERLAY_MIN_WIDTH_VARIABLE))\n return Number.isFinite(minWidth) ? minWidth : DEFAULT_OVERLAY_MIN_WIDTH\n}\n\nfunction clearManagedWidthStyles(overlay: HTMLElement) {\n overlay.style.removeProperty('width')\n overlay.style.removeProperty('min-width')\n}\n\nfunction createWidthMiddleware(\n overlay: HTMLElement,\n options: Required<OverlayOptions>,\n widthStyles: WidthStyleState\n): Middleware | undefined {\n if (options.matchWidth) {\n // matchWidth 必须移除之前 minAnchorWidth 写入的 min-width,才能精确跟随 trigger。\n overlay.style.removeProperty('min-width')\n widthStyles.hasManagedStyles = true\n return size({\n apply({ rects }) {\n overlay.style.width = `${rects.reference.width}px`\n }\n })\n }\n\n if (options.minAnchorWidth) {\n const minWidth = readOverlayMinWidth(overlay)\n widthStyles.hasManagedStyles = true\n return size({\n apply({ rects }) {\n // max-content 让内容决定宽度,min-width 再提供 trigger 与 floor 两个下限。\n overlay.style.width = 'max-content'\n overlay.style.minWidth = `${Math.max(rects.reference.width, minWidth)}px`\n }\n })\n }\n\n if (widthStyles.hasManagedStyles) {\n clearManagedWidthStyles(overlay)\n widthStyles.hasManagedStyles = false\n }\n\n return undefined\n}\n\nfunction createPositioningMiddleware(\n overlay: HTMLElement,\n options: Required<OverlayOptions>,\n widthStyles: WidthStyleState\n): Middleware[] {\n const middleware: Middleware[] = [offset(options.offset)]\n if (options.flip) middleware.push(flip())\n if (options.shift) middleware.push(shift({ padding: 8 }))\n\n const widthMiddleware = createWidthMiddleware(overlay, options, widthStyles)\n if (widthMiddleware) middleware.push(widthMiddleware)\n\n return middleware\n}\n\nfunction getTransformOrigin(placement: Placement): string {\n const [side, alignment] = placement.split('-')\n const horizontalOrigin = alignment === 'start' ? 'left' : alignment === 'end' ? 'right' : 'center'\n const verticalOrigin = alignment === 'start' ? 'top' : alignment === 'end' ? 'bottom' : 'center'\n\n if (side === 'top') return `bottom ${horizontalOrigin}`\n if (side === 'bottom') return `top ${horizontalOrigin}`\n if (side === 'left') return `right ${verticalOrigin}`\n return `left ${verticalOrigin}`\n}\n\nexport const defineOverlay = () =>\n definePlugin<OverlayApi, { anchor: OverlayAnchor; overlay: HTMLElement } & OverlayOptions>(ctx => {\n const options: Required<OverlayOptions> = {\n placement: ctx.placement ?? DEFAULT_OPTIONS.placement,\n offset: ctx.offset ?? DEFAULT_OPTIONS.offset,\n flip: ctx.flip ?? DEFAULT_OPTIONS.flip,\n shift: ctx.shift ?? DEFAULT_OPTIONS.shift,\n matchWidth: ctx.matchWidth ?? DEFAULT_OPTIONS.matchWidth,\n minAnchorWidth: ctx.minAnchorWidth ?? DEFAULT_OPTIONS.minAnchorWidth,\n strategy: ctx.strategy ?? DEFAULT_OPTIONS.strategy\n }\n\n const overlay = ctx.overlay\n const widthStyles: WidthStyleState = { hasManagedStyles: false }\n let isOpen = false\n let cleanupAutoUpdate: (() => void) | null = null\n let currentAnchor = ctx.anchor\n\n function updatePosition() {\n const middleware = createPositioningMiddleware(overlay, options, widthStyles)\n\n const applyPosition = () => {\n void computePosition(currentAnchor, overlay, {\n placement: options.placement,\n strategy: options.strategy,\n middleware\n }).then(({ x, y, placement }) => {\n overlay.style.left = `${x}px`\n overlay.style.top = `${y}px`\n overlay.style.setProperty('--wui-internal-overlay-transform-origin', getTransformOrigin(placement))\n })\n }\n\n // 立即定位一次,避免浮层首帧出现在左上角;autoUpdate 负责后续滚动/尺寸变化。\n applyPosition()\n cleanupAutoUpdate?.()\n cleanupAutoUpdate = autoUpdate(currentAnchor, overlay, applyPosition)\n }\n\n return {\n isOpen() {\n return isOpen\n },\n options,\n\n open() {\n if (isOpen) return\n isOpen = true\n overlay.style.display = ''\n updatePosition()\n },\n\n close() {\n if (!isOpen) return\n isOpen = false\n cleanupAutoUpdate?.()\n cleanupAutoUpdate = null\n },\n\n toggle() {\n if (isOpen) this.close()\n else this.open()\n },\n\n update(nextOptions) {\n Object.assign(options, nextOptions)\n if (isOpen) updatePosition()\n },\n\n updateAnchor(newAnchor: OverlayAnchor) {\n currentAnchor = newAnchor\n if (isOpen) updatePosition()\n },\n\n dispose() {\n this.close()\n }\n }\n })\n"],"mappings":"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"}
1
+ {"version":3,"file":"overlay.js","names":[],"sources":["../../../src/shared/overlay/overlay.ts"],"sourcesContent":["import {\n autoUpdate,\n computePosition,\n flip,\n offset,\n shift,\n size,\n type Middleware,\n type Placement,\n type Strategy\n} from '@floating-ui/dom'\nimport { definePlugin } from '@greypan/js-kit'\n\nexport interface OverlayOptions {\n placement?: Placement\n offset?: number\n flip?: boolean\n shift?: boolean\n /** 面板宽度与锚点宽度保持一致。 */\n matchWidth?: boolean\n /** 面板至少与锚点同宽,同时允许内容将面板撑宽。 */\n minAnchorWidth?: boolean\n strategy?: Strategy\n}\n\n/** 虚拟锚点:无对应 DOM 元素的定位基准。context-menu 未走 defineOverlay(定位语义分歧见 ADR-0046)。 */\nexport interface OverlayVirtualAnchor {\n getBoundingClientRect(): DOMRect\n}\n\nexport type OverlayAnchor = HTMLElement | OverlayVirtualAnchor\n\nexport interface OverlayApi {\n isOpen(): boolean\n readonly options: Required<OverlayOptions>\n open(): void\n close(): void\n toggle(): void\n update(options: Partial<OverlayOptions>): void\n updateAnchor(anchor: OverlayAnchor): void\n dispose(): void\n}\n\nconst DEFAULT_OVERLAY_MIN_WIDTH = 120\nconst OVERLAY_MIN_WIDTH_VARIABLE = '--wui-overlay-min-width'\n\nconst DEFAULT_OPTIONS: Required<OverlayOptions> = {\n placement: 'bottom-start',\n offset: 4,\n flip: true,\n shift: true,\n matchWidth: false,\n minAnchorWidth: false,\n strategy: 'absolute'\n}\n\ninterface WidthStyleState {\n hasManagedStyles: boolean\n}\n\nfunction readOverlayMinWidth(overlay: HTMLElement): number {\n if (typeof getComputedStyle !== 'function') return DEFAULT_OVERLAY_MIN_WIDTH\n\n const minWidth = Number.parseFloat(getComputedStyle(overlay).getPropertyValue(OVERLAY_MIN_WIDTH_VARIABLE))\n return Number.isFinite(minWidth) ? minWidth : DEFAULT_OVERLAY_MIN_WIDTH\n}\n\nfunction clearManagedWidthStyles(overlay: HTMLElement) {\n overlay.style.removeProperty('width')\n overlay.style.removeProperty('min-width')\n}\n\nfunction createWidthMiddleware(\n overlay: HTMLElement,\n options: Required<OverlayOptions>,\n widthStyles: WidthStyleState\n): Middleware | undefined {\n if (options.matchWidth) {\n // matchWidth 必须移除之前 minAnchorWidth 写入的 min-width,才能精确跟随 trigger。\n overlay.style.removeProperty('min-width')\n widthStyles.hasManagedStyles = true\n return size({\n apply({ rects }) {\n overlay.style.width = `${rects.reference.width}px`\n }\n })\n }\n\n if (options.minAnchorWidth) {\n const minWidth = readOverlayMinWidth(overlay)\n widthStyles.hasManagedStyles = true\n return size({\n apply({ rects }) {\n // max-content 让内容决定宽度,min-width 再提供 trigger 与 floor 两个下限。\n overlay.style.width = 'max-content'\n overlay.style.minWidth = `${Math.max(rects.reference.width, minWidth)}px`\n }\n })\n }\n\n if (widthStyles.hasManagedStyles) {\n clearManagedWidthStyles(overlay)\n widthStyles.hasManagedStyles = false\n }\n\n return undefined\n}\n\nfunction createPositioningMiddleware(\n overlay: HTMLElement,\n options: Required<OverlayOptions>,\n widthStyles: WidthStyleState\n): Middleware[] {\n const middleware: Middleware[] = [offset(options.offset)]\n if (options.flip) middleware.push(flip())\n if (options.shift) middleware.push(shift({ padding: 8 }))\n\n const widthMiddleware = createWidthMiddleware(overlay, options, widthStyles)\n if (widthMiddleware) middleware.push(widthMiddleware)\n\n return middleware\n}\n\nfunction getTransformOrigin(placement: Placement): string {\n const [side, alignment] = placement.split('-')\n const horizontalOrigin = alignment === 'start' ? 'left' : alignment === 'end' ? 'right' : 'center'\n const verticalOrigin = alignment === 'start' ? 'top' : alignment === 'end' ? 'bottom' : 'center'\n\n if (side === 'top') return `bottom ${horizontalOrigin}`\n if (side === 'bottom') return `top ${horizontalOrigin}`\n if (side === 'left') return `right ${verticalOrigin}`\n return `left ${verticalOrigin}`\n}\n\nexport const defineOverlay = () =>\n definePlugin<OverlayApi, { anchor: OverlayAnchor; overlay: HTMLElement } & OverlayOptions>(ctx => {\n const options: Required<OverlayOptions> = {\n placement: ctx.placement ?? DEFAULT_OPTIONS.placement,\n offset: ctx.offset ?? DEFAULT_OPTIONS.offset,\n flip: ctx.flip ?? DEFAULT_OPTIONS.flip,\n shift: ctx.shift ?? DEFAULT_OPTIONS.shift,\n matchWidth: ctx.matchWidth ?? DEFAULT_OPTIONS.matchWidth,\n minAnchorWidth: ctx.minAnchorWidth ?? DEFAULT_OPTIONS.minAnchorWidth,\n strategy: ctx.strategy ?? DEFAULT_OPTIONS.strategy\n }\n\n const overlay = ctx.overlay\n const widthStyles: WidthStyleState = { hasManagedStyles: false }\n let isOpen = false\n let cleanupAutoUpdate: (() => void) | null = null\n let currentAnchor = ctx.anchor\n\n function updatePosition() {\n const middleware = createPositioningMiddleware(overlay, options, widthStyles)\n\n const applyPosition = () => {\n void computePosition(currentAnchor, overlay, {\n placement: options.placement,\n strategy: options.strategy,\n middleware\n }).then(({ x, y, placement }) => {\n overlay.style.left = `${x}px`\n overlay.style.top = `${y}px`\n overlay.style.setProperty('--wui-internal-overlay-transform-origin', getTransformOrigin(placement))\n })\n }\n\n // 立即定位一次,避免浮层首帧出现在左上角;autoUpdate 负责后续滚动/尺寸变化。\n applyPosition()\n cleanupAutoUpdate?.()\n cleanupAutoUpdate = autoUpdate(currentAnchor, overlay, applyPosition)\n }\n\n return {\n isOpen() {\n return isOpen\n },\n options,\n\n open() {\n if (isOpen) return\n isOpen = true\n overlay.style.display = ''\n updatePosition()\n },\n\n close() {\n if (!isOpen) return\n isOpen = false\n cleanupAutoUpdate?.()\n cleanupAutoUpdate = null\n },\n\n toggle() {\n if (isOpen) this.close()\n else this.open()\n },\n\n update(nextOptions) {\n Object.assign(options, nextOptions)\n if (isOpen) updatePosition()\n },\n\n updateAnchor(newAnchor: OverlayAnchor) {\n currentAnchor = newAnchor\n if (isOpen) updatePosition()\n },\n\n dispose() {\n this.close()\n }\n }\n })\n"],"mappings":"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"}
@@ -9,42 +9,109 @@ function i(e, t, n) {
9
9
  n && e.style.setProperty(t, n);
10
10
  }
11
11
  }
12
- var a = () => r((e) => {
12
+ var a = "wui-portal-content-anchor", o = () => r((e) => {
13
13
  let t = document.createElement("div");
14
14
  t.style.display = "contents";
15
15
  let n = t.attachShadow({ mode: "open" }), r = document.createElement("style");
16
16
  r.textContent = e.style;
17
17
  let i = document.createElement("div");
18
18
  i.className = e.className, i.dataset.wuiPresence = "entering", n.append(r, i);
19
- let a = [], o = new MutationObserver((t) => e.onContentChange?.(t)), s = (e) => {
20
- e.forEach((e) => {
21
- let t = a.indexOf(e);
22
- t >= 0 && a.splice(t, 1);
23
- });
19
+ let o = [], s = new MutationObserver((t) => {
20
+ f(t), e.onContentChange?.(t);
21
+ }), c = (e) => {
22
+ let t = e.parentNode;
23
+ if (!t) return null;
24
+ let n = document.createComment(a);
25
+ t.insertBefore(n, e);
26
+ let r = {
27
+ node: e,
28
+ marker: n
29
+ };
30
+ return o.push(r), r;
31
+ }, l = (e) => {
32
+ for (let t of e) {
33
+ let e = o.findIndex((e) => e.node === t);
34
+ e >= 0 && (o[e].marker.remove(), o.splice(e, 1));
35
+ }
36
+ }, d = (t) => {
37
+ let n = [];
38
+ for (let r of Array.from(e.target.childNodes)) if (r instanceof Comment && r.textContent === a) {
39
+ let e = o.find((e) => e.marker === r);
40
+ e && e.node.parentNode === t && n.push(e.node);
41
+ }
42
+ if (n.length === 0) return;
43
+ let r = Array.from(t.childNodes).filter((e) => n.includes(e) || !(e instanceof Comment));
44
+ if (!(r.length === n.length && r.every((e, t) => e === n[t]))) for (let e = n.length - 1; e >= 0; e--) {
45
+ let r = n[e], i = n[e + 1];
46
+ i ? r.nextElementSibling !== i && t.insertBefore(r, i) : t.appendChild(r);
47
+ }
48
+ }, f = (t) => {
49
+ let n = (e) => {
50
+ let t = e;
51
+ for (; t && t !== i;) {
52
+ if (o.some((e) => e.node === t)) return !0;
53
+ t = t.parentNode;
54
+ }
55
+ return !1;
56
+ }, r = new Set(t.flatMap((e) => [...e.addedNodes])), s = [];
57
+ for (let e of t) for (let t of e.addedNodes) !(t instanceof Comment) || t.textContent === a || i.contains(t) && (n(e.target) || s.push(t));
58
+ if (!s.length) return;
59
+ let c = Array.from(e.target.childNodes), l = o.filter((e) => !r.has(e.node) && t.some((t) => [...t.removedNodes].includes(e.node))).sort((e, t) => c.indexOf(e.marker) - c.indexOf(t.marker));
60
+ for (let [t, n] of s.entries()) {
61
+ let r = l[Math.min(t, l.length - 1)]?.marker;
62
+ r && r.parentNode === e.target ? e.target.insertBefore(n, r) : e.target.appendChild(n);
63
+ }
24
64
  };
25
- return c(e.container, e.target).appendChild(t), e.onContentChange && o.observe(i, {
65
+ u(e.container, e.target).appendChild(t), e.onContentChange && s.observe(i, {
26
66
  childList: !0,
27
67
  subtree: !0
28
- }), {
68
+ });
69
+ let p = e.migrateAddedNodes, m = new MutationObserver((e) => {
70
+ let t = new Set(e.flatMap((e) => [...e.addedNodes])), n = e.flatMap((e) => [...e.removedNodes]).filter((e) => e instanceof Comment && e.textContent === a && !t.has(e)).map((e) => o.find((t) => t.marker === e)?.node).filter((e) => e !== void 0);
71
+ if (n.length) {
72
+ h.removeContent(n);
73
+ for (let e of n) e.parentNode?.removeChild(e);
74
+ }
75
+ if (!p) return;
76
+ let r = p(e.flatMap((e) => [...e.addedNodes]));
77
+ r.length && h.appendContent(r);
78
+ });
79
+ m.observe(e.target, { childList: !0 });
80
+ let h = {
29
81
  panel: i,
30
82
  moveContent(e, t = i) {
31
- a.splice(0, a.length, ...e), t.append(...a);
83
+ o.forEach((e) => e.marker.remove());
84
+ let n = e.map(c).filter((e) => e !== null);
85
+ o.length = 0, o.push(...n), t.append(...e), d(t);
32
86
  },
33
87
  appendContent(e, t = i) {
34
- s(e), a.push(...e), t.append(...e);
88
+ for (let n of e) {
89
+ l([n]);
90
+ let e = c(n);
91
+ e && t.appendChild(e.node);
92
+ }
93
+ d(t);
35
94
  },
36
95
  removeContent(e) {
37
- s(e);
96
+ l(e);
38
97
  },
39
98
  restoreContent() {
40
- e.target.append(...a), a.length = 0;
99
+ m.disconnect();
100
+ for (let t of o) {
101
+ let n = [], r = t.node.previousSibling;
102
+ for (; r instanceof Comment && r.textContent !== a;) n.unshift(r), r = r.previousSibling;
103
+ for (let r of n) t.marker.parentNode === e.target ? e.target.insertBefore(r, t.marker) : e.target.appendChild(r);
104
+ t.marker.parentNode === e.target ? e.target.insertBefore(t.node, t.marker) : e.target.appendChild(t.node), t.marker.remove();
105
+ }
106
+ o.length = 0;
41
107
  },
42
108
  remove() {
43
- o.disconnect(), t.remove();
109
+ m.disconnect(), s.disconnect(), o.forEach((e) => e.marker.remove()), o.length = 0, t.remove();
44
110
  }
45
111
  };
112
+ return h;
46
113
  });
47
- function o(e, t, n) {
114
+ function s(e, t, n) {
48
115
  let r = e;
49
116
  for (; r && r !== n;) {
50
117
  if (r === t) return !0;
@@ -52,7 +119,12 @@ function o(e, t, n) {
52
119
  }
53
120
  return !1;
54
121
  }
55
- function s(e) {
122
+ function c(e) {
123
+ if (e.parentNode) return e.parentNode;
124
+ let t = e.getRootNode();
125
+ return t instanceof ShadowRoot ? t.host : null;
126
+ }
127
+ function l(e) {
56
128
  let t = e;
57
129
  for (; t && t !== document.documentElement;) {
58
130
  if (t instanceof HTMLDialogElement && t.open) return t;
@@ -61,17 +133,17 @@ function s(e) {
61
133
  for (let r of n) {
62
134
  if (!(r instanceof HTMLDialogElement) || !r.open) continue;
63
135
  let n = r.querySelectorAll("slot");
64
- for (let i of n) for (let n of i.assignedNodes()) if (n instanceof Node && o(e, n, t)) return r;
136
+ for (let i of n) for (let n of i.assignedNodes()) if (n instanceof Node && s(e, n, t)) return r;
65
137
  }
66
138
  }
67
- t = t.parentNode;
139
+ t = c(t);
68
140
  }
69
141
  return null;
70
142
  }
71
- function c(r, i, a = {}) {
72
- return (typeof r == "function" ? r() : r) || s(i) || ((a.preferRootTheme ? n() : t(i))?.getOverlayRoot() ?? e());
143
+ function u(r, i, a = {}) {
144
+ return (typeof r == "function" ? r() : r) || l(i) || ((a.preferRootTheme ? n() : t(i))?.getOverlayRoot() ?? e());
73
145
  }
74
146
  //#endregion
75
- export { i as applyOverlayVariables, a as defineOverlayPortal, c as resolveOverlayContainer };
147
+ export { i as applyOverlayVariables, o as defineOverlayPortal, u as resolveOverlayContainer };
76
148
 
77
149
  //# sourceMappingURL=portal.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"portal.js","names":[],"sources":["../../../src/shared/overlay/portal.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\n\nimport { getFallbackOverlayRoot } from '@/shared/theme/overlay-root'\nimport { findNearestTheme, findRootTheme } from '@/shared/theme/theme-scope'\n\nexport type OverlayContainer = HTMLElement | (() => HTMLElement | undefined)\n\nexport interface OverlayContainerResolutionOptions {\n /** 无 target 时优先使用文档中的 root theme。 */\n preferRootTheme?: boolean\n}\n\nexport interface OverlayPortalOptions {\n container?: OverlayContainer\n target: Element\n style: string\n className: string\n onContentChange?: (mutations: MutationRecord[]) => void\n}\n\nexport interface OverlayPortal {\n readonly panel: HTMLElement\n restoreContent(): void\n moveContent(nodes: Node[], target?: HTMLElement): void\n appendContent(nodes: Node[], target?: HTMLElement): void\n removeContent(nodes: Node[]): void\n remove(): void\n}\n\n/**\n * Portal 面板位于独立 shadow root,宿主上的 CSS 自定义属性不会沿渲染树继承到 panel。\n * 这里在创建时镜像锚点解析后的变量,让组件 host 上的尺寸配置对 portal 仍然生效。\n */\nexport function applyOverlayVariables(panel: HTMLElement, source: Element, variables: readonly string[]): void {\n const computedStyle = getComputedStyle(source)\n for (const variable of variables) {\n const value = computedStyle.getPropertyValue(variable).trim()\n if (value) panel.style.setProperty(variable, value)\n }\n}\n\n/**\n * 构建带 Shadow DOM 样式边界的 Portal 面板,并追踪被迁移的内容节点以便恢复。\n */\nexport const defineOverlayPortal = () =>\n definePlugin<OverlayPortal, OverlayPortalOptions>(ctx => {\n const host = document.createElement('div')\n // :host 规则可能让宿主在 overlay 容器中生成行盒;display: contents 保留面板定位而不参与布局。\n host.style.display = 'contents'\n\n const root = host.attachShadow({ mode: 'open' })\n const style = document.createElement('style')\n style.textContent = ctx.style\n const panel = document.createElement('div')\n panel.className = ctx.className\n panel.dataset.wuiPresence = 'entering'\n root.append(style, panel)\n\n const trackedNodes: Node[] = []\n const contentObserver = new MutationObserver(mutations => ctx.onContentChange?.(mutations))\n const untrackNodes = (nodes: Node[]) => {\n nodes.forEach(node => {\n const index = trackedNodes.indexOf(node)\n if (index >= 0) trackedNodes.splice(index, 1)\n })\n }\n\n resolveOverlayContainer(ctx.container, ctx.target).appendChild(host)\n if (ctx.onContentChange) contentObserver.observe(panel, { childList: true, subtree: true })\n\n return {\n panel,\n\n moveContent(nodes, target = panel) {\n trackedNodes.splice(0, trackedNodes.length, ...nodes)\n target.append(...trackedNodes)\n },\n\n appendContent(nodes, target = panel) {\n untrackNodes(nodes)\n trackedNodes.push(...nodes)\n target.append(...nodes)\n },\n\n removeContent(nodes) {\n untrackNodes(nodes)\n },\n\n restoreContent() {\n ctx.target.append(...trackedNodes)\n trackedNodes.length = 0\n },\n\n remove() {\n contentObserver.disconnect()\n host.remove()\n }\n }\n })\n\n/**\n * 判断 node 是否是 slot 已分配节点(或其 light DOM 后代),即 node 实际渲染在 shadowRoot 内。\n * slotted 内容的 parentNode 仍停留在 light DOM,因此不能用普通祖先遍历跨过 shadow 边界,\n * 只能通过 slot.assignedNodes() 判断内容被渲染到哪个 slot。\n */\nfunction isAssignedIntoTarget(node: Node, assignedNode: Node, boundary: Node): boolean {\n let current: Node | null = node\n while (current && current !== boundary) {\n if (current === assignedNode) return true\n current = current.parentNode\n }\n return false\n}\n\n/**\n * 查找包含 target 且处于打开态的最近原生 <dialog>。\n * 原生 showModal() 会把 dialog 提升到浏览器 top layer,而常规 overlay 容器在普通文档流,\n * 二者无法靠 z-index 叠序——overlay 会被 top layer 的 dialog 遮住。\n * 因此 target 被渲染到某个已打开的原生 dialog 内时,应把 overlay 挂到该 dialog 上,\n * 使其一并进入 top layer(抽屉 / 对话框内的 dropdown、tooltip、context-menu 均适用)。\n */\nfunction findEnclosingOpenDialog(target: Element): HTMLDialogElement | null {\n let current: Node | null = target\n while (current && current !== document.documentElement) {\n if (current instanceof HTMLDialogElement && current.open) return current\n\n // 跨过 shadow host:检查其 shadowRoot 内已打开的 dialog 是否渲染了 target。\n if (current instanceof Element && current.shadowRoot) {\n const dialogs = current.shadowRoot.querySelectorAll('dialog')\n for (const dialog of dialogs) {\n if (!(dialog instanceof HTMLDialogElement) || !dialog.open) continue\n const slots = dialog.querySelectorAll('slot')\n for (const slot of slots) {\n for (const assigned of slot.assignedNodes()) {\n if (!(assigned instanceof Node)) continue\n if (isAssignedIntoTarget(target, assigned, current)) return dialog\n }\n }\n }\n }\n\n current = current.parentNode\n }\n return null\n}\n\nexport function resolveOverlayContainer(\n container: OverlayContainer | undefined,\n target: Element,\n options: OverlayContainerResolutionOptions = {}\n): HTMLElement {\n const explicit = typeof container === 'function' ? container() : container\n if (explicit) return explicit\n\n // target 在已打开的原生 dialog / drawer 内:挂到该 dialog,加入 top layer。\n const enclosingDialog = findEnclosingOpenDialog(target)\n if (enclosingDialog) return enclosingDialog\n\n // 无 target 的调用(如菜单)优先使用 root theme;有 target 时使用最近的 theme。\n const theme = options.preferRootTheme ? findRootTheme() : findNearestTheme(target)\n return theme?.getOverlayRoot() ?? getFallbackOverlayRoot()\n}\n"],"mappings":";;;;AAiCA,SAAgB,EAAsB,GAAoB,GAAiB,GAAoC;CAC7G,IAAM,IAAgB,iBAAiB,CAAM;CAC7C,KAAK,IAAM,KAAY,GAAW;EAChC,IAAM,IAAQ,EAAc,iBAAiB,CAAQ,CAAC,CAAC,KAAK;EAC5D,AAAI,KAAO,EAAM,MAAM,YAAY,GAAU,CAAK;CACpD;AACF;AAKA,IAAa,UACX,GAAkD,MAAO;CACvD,IAAM,IAAO,SAAS,cAAc,KAAK;CAEzC,EAAK,MAAM,UAAU;CAErB,IAAM,IAAO,EAAK,aAAa,EAAE,MAAM,OAAO,CAAC,GACzC,IAAQ,SAAS,cAAc,OAAO;CAC5C,EAAM,cAAc,EAAI;CACxB,IAAM,IAAQ,SAAS,cAAc,KAAK;CAG1C,AAFA,EAAM,YAAY,EAAI,WACtB,EAAM,QAAQ,cAAc,YAC5B,EAAK,OAAO,GAAO,CAAK;CAExB,IAAM,IAAuB,CAAC,GACxB,IAAkB,IAAI,kBAAiB,MAAa,EAAI,kBAAkB,CAAS,CAAC,GACpF,KAAgB,MAAkB;EACtC,EAAM,SAAQ,MAAQ;GACpB,IAAM,IAAQ,EAAa,QAAQ,CAAI;GACvC,AAAI,KAAS,KAAG,EAAa,OAAO,GAAO,CAAC;EAC9C,CAAC;CACH;CAKA,OAHA,EAAwB,EAAI,WAAW,EAAI,MAAM,CAAC,CAAC,YAAY,CAAI,GAC/D,EAAI,mBAAiB,EAAgB,QAAQ,GAAO;EAAE,WAAW;EAAM,SAAS;CAAK,CAAC,GAEnF;EACL;EAEA,YAAY,GAAO,IAAS,GAAO;GAEjC,AADA,EAAa,OAAO,GAAG,EAAa,QAAQ,GAAG,CAAK,GACpD,EAAO,OAAO,GAAG,CAAY;EAC/B;EAEA,cAAc,GAAO,IAAS,GAAO;GAGnC,AAFA,EAAa,CAAK,GAClB,EAAa,KAAK,GAAG,CAAK,GAC1B,EAAO,OAAO,GAAG,CAAK;EACxB;EAEA,cAAc,GAAO;GACnB,EAAa,CAAK;EACpB;EAEA,iBAAiB;GAEf,AADA,EAAI,OAAO,OAAO,GAAG,CAAY,GACjC,EAAa,SAAS;EACxB;EAEA,SAAS;GAEP,AADA,EAAgB,WAAW,GAC3B,EAAK,OAAO;EACd;CACF;AACF,CAAC;AAOH,SAAS,EAAqB,GAAY,GAAoB,GAAyB;CACrF,IAAI,IAAuB;CAC3B,OAAO,KAAW,MAAY,IAAU;EACtC,IAAI,MAAY,GAAc,OAAO;EACrC,IAAU,EAAQ;CACpB;CACA,OAAO;AACT;AASA,SAAS,EAAwB,GAA2C;CAC1E,IAAI,IAAuB;CAC3B,OAAO,KAAW,MAAY,SAAS,kBAAiB;EACtD,IAAI,aAAmB,qBAAqB,EAAQ,MAAM,OAAO;EAGjE,IAAI,aAAmB,WAAW,EAAQ,YAAY;GACpD,IAAM,IAAU,EAAQ,WAAW,iBAAiB,QAAQ;GAC5D,KAAK,IAAM,KAAU,GAAS;IAC5B,IAAI,EAAE,aAAkB,sBAAsB,CAAC,EAAO,MAAM;IAC5D,IAAM,IAAQ,EAAO,iBAAiB,MAAM;IAC5C,KAAK,IAAM,KAAQ,GACjB,KAAK,IAAM,KAAY,EAAK,cAAc,GAClC,iBAAoB,QACtB,EAAqB,GAAQ,GAAU,CAAO,GAAG,OAAO;GAGlE;EACF;EAEA,IAAU,EAAQ;CACpB;CACA,OAAO;AACT;AAEA,SAAgB,EACd,GACA,GACA,IAA6C,CAAC,GACjC;CAUb,QATiB,OAAO,KAAc,aAAa,EAAU,IAAI,MAIzC,EAAwB,CAC5C,OAGU,EAAQ,kBAAkB,EAAc,IAAI,EAAiB,CAAM,EAAA,EACnE,eAAe,KAAK,EAAuB;AAC3D"}
1
+ {"version":3,"file":"portal.js","names":[],"sources":["../../../src/shared/overlay/portal.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\n\nimport { getFallbackOverlayRoot } from '@/shared/theme/overlay-root'\nimport { findNearestTheme, findRootTheme } from '@/shared/theme/theme-scope'\n\nexport type OverlayContainer = HTMLElement | (() => HTMLElement | undefined)\n\nexport interface OverlayContainerResolutionOptions {\n /** 无 target 时优先使用文档中的 root theme。 */\n preferRootTheme?: boolean\n}\n\nexport interface OverlayPortalOptions {\n container?: OverlayContainer\n target: Element\n style: string\n className: string\n onContentChange?: (mutations: MutationRecord[]) => void\n /**\n * 打开期实时渲染契约:框架(Vue/React 条件渲染)可能在浮层打开期间向宿主\n * light DOM 持续插入节点。提供该回调时,portal 监听宿主直接子节点的 childList,\n * 把回调认定的可迁移节点增量迁入面板;关闭恢复时与初始迁移节点一样按原位回插。\n * 回调应只返回渲染性内容(元素/文本),框架注释锚点必须留在宿主,\n * 否则框架后续 patch 会以面板内节点为插入基准。\n * 返回空数组表示本次新增无需迁移。\n */\n migrateAddedNodes?: (addedNodes: Node[]) => Node[]\n}\n\nexport interface OverlayPortal {\n readonly panel: HTMLElement\n restoreContent(): void\n moveContent(nodes: Node[], target?: HTMLElement): void\n appendContent(nodes: Node[], target?: HTMLElement): void\n removeContent(nodes: Node[]): void\n remove(): void\n}\n\n/**\n * Portal 面板位于独立 shadow root,宿主上的 CSS 自定义属性不会沿渲染树继承到 panel。\n * 这里在创建时镜像锚点解析后的变量,让组件 host 上的尺寸配置对 portal 仍然生效。\n */\nexport function applyOverlayVariables(panel: HTMLElement, source: Element, variables: readonly string[]): void {\n const computedStyle = getComputedStyle(source)\n for (const variable of variables) {\n const value = computedStyle.getPropertyValue(variable).trim()\n if (value) panel.style.setProperty(variable, value)\n }\n}\n\n/**\n * 迁移节点的追踪条目。每个节点迁入面板时在宿主 light DOM 原位留一个 marker 注释:\n * 宿主因此始终持有完整有序的骨架(marker + 框架注释锚点 + 未迁移节点),\n * 框架(Vue/React 条件渲染)打开期的插入以宿主为容器、以真实锚点为基准;\n * 面板内节点按宿主骨架序重排(中段插入不落到面板末尾);关闭时按 marker 位\n * 双向归还、marker 摘除。机制与 menu-tree 的托管菜单项锚一致(context-menu/dropdown 共用)。\n */\ninterface TrackedNodeEntry {\n node: Node\n marker: Comment\n}\n\nconst PORTAL_CONTENT_MARKER = 'wui-portal-content-anchor'\n\n/**\n * 构建带 Shadow DOM 样式边界的 Portal 面板,并追踪被迁移的内容节点以便恢复。\n */\nexport const defineOverlayPortal = () =>\n definePlugin<OverlayPortal, OverlayPortalOptions>(ctx => {\n const host = document.createElement('div')\n // :host 规则可能让宿主在 overlay 容器中生成行盒;display: contents 保留面板定位而不参与布局。\n host.style.display = 'contents'\n\n const root = host.attachShadow({ mode: 'open' })\n const style = document.createElement('style')\n style.textContent = ctx.style\n const panel = document.createElement('div')\n panel.className = ctx.className\n panel.dataset.wuiPresence = 'entering'\n root.append(style, panel)\n\n const trackedNodes: TrackedNodeEntry[] = []\n const contentObserver = new MutationObserver(mutations => {\n // 内部不变量先于消费方回调执行:配对依赖 trackedNodes 尚未被消费方 untrack 的状态。\n returnStrayFrameworkComments(mutations)\n ctx.onContentChange?.(mutations)\n })\n // 同一 observer 批次内被插入又移除的游离节点没有宿主位可锚,不追踪不迁移\n // (insertBefore 要求 node 是 parent 的子节点);其归宿由框架的删除语义决定。\n const recordEntry = (node: Node): TrackedNodeEntry | null => {\n const parent = node.parentNode\n if (!parent) return null\n const marker = document.createComment(PORTAL_CONTENT_MARKER)\n parent.insertBefore(marker, node)\n const entry: TrackedNodeEntry = { node, marker }\n trackedNodes.push(entry)\n return entry\n }\n const untrackNodes = (nodes: Node[]) => {\n for (const node of nodes) {\n const index = trackedNodes.findIndex(entry => entry.node === node)\n if (index >= 0) {\n trackedNodes[index].marker.remove()\n trackedNodes.splice(index, 1)\n }\n }\n }\n // 面板内节点按宿主骨架序重排:骨架 = 宿主 childNodes 中本 portal 的 marker 序。\n // 只在已跟踪节点之间移动(insertBefore 相对定位),不触碰面板内的组件自有节点\n // (如 autocomplete 的空态占位);末位节点 append,越过面板内注释锚点的情形由\n // restoreContent 的相邻注释随迁兜底。\n const orderTargetBySkeleton = (target: HTMLElement) => {\n const skeleton: Node[] = []\n for (const node of Array.from(ctx.target.childNodes)) {\n if (node instanceof Comment && node.textContent === PORTAL_CONTENT_MARKER) {\n const entry = trackedNodes.find(e => e.marker === node)\n if (entry && entry.node.parentNode === target) skeleton.push(entry.node)\n }\n }\n if (skeleton.length === 0) return\n const current = Array.from(target.childNodes).filter(\n node => skeleton.includes(node) || !(node instanceof Comment)\n )\n if (current.length === skeleton.length && current.every((node, index) => node === skeleton[index])) return\n for (let index = skeleton.length - 1; index >= 0; index--) {\n const node = skeleton[index]\n const next = skeleton[index + 1]\n if (next) {\n if ((node as Element).nextElementSibling !== next) target.insertBefore(node, next)\n } else {\n target.appendChild(node)\n }\n }\n }\n\n /**\n * 打开期不变量:框架删除已迁移节点时,其条件渲染占位注释(v-if 等)会被插进\n * 面板内容区而不是宿主——block 子树 patch 以 hostParentNode(oldVNode.el) 重定\n * container,而旧 el 已在面板(可能是 panel 自身或组件的内容子容器)。\n * 注释是框架后续翻转依赖的 vnode.el:留在面板会随面板销毁,下次翻转把内容插进\n * 已脱离文档的旧面板(内容\"既不在宿主也不在面板\"且无法恢复)。\n * 每个面板 mutation 批次内即时归还:与同批次被框架删除的 tracked 节点按宿主\n * marker 序配对,插到其 marker 位(多注释对多删除为近似配对;无配对时落宿主\n * 骨架末尾兜底)。tracked 子树内的注释是迁移内容自身的嵌套条件渲染,不属游离\n * 锚点,必须留在原位;随整个子树被删的注释已不在面板内,无需救援。\n */\n const returnStrayFrameworkComments = (mutations: MutationRecord[]) => {\n const isInsideTrackedSubtree = (start: Node): boolean => {\n let current: Node | null = start\n while (current && current !== panel) {\n if (trackedNodes.some(entry => entry.node === current)) return true\n current = current.parentNode\n }\n return false\n }\n const addedNodes = new Set(mutations.flatMap(mutation => [...mutation.addedNodes]))\n const strays: Comment[] = []\n for (const mutation of mutations) {\n for (const node of mutation.addedNodes) {\n if (!(node instanceof Comment) || node.textContent === PORTAL_CONTENT_MARKER) continue\n // 同批次移入又移除的游离注释、随删除子树一起消失的注释都不在面板内,跳过\n if (!panel.contains(node)) continue\n if (isInsideTrackedSubtree(mutation.target)) continue\n strays.push(node)\n }\n }\n if (!strays.length) return\n const hostChildren = Array.from(ctx.target.childNodes)\n const removedEntries = trackedNodes\n .filter(\n entry =>\n !addedNodes.has(entry.node) && mutations.some(mutation => [...mutation.removedNodes].includes(entry.node))\n )\n .sort((a, b) => hostChildren.indexOf(a.marker) - hostChildren.indexOf(b.marker))\n for (const [index, comment] of strays.entries()) {\n const marker = removedEntries[Math.min(index, removedEntries.length - 1)]?.marker\n if (marker && marker.parentNode === ctx.target) ctx.target.insertBefore(comment, marker)\n else ctx.target.appendChild(comment)\n }\n }\n\n resolveOverlayContainer(ctx.container, ctx.target).appendChild(host)\n if (ctx.onContentChange) contentObserver.observe(panel, { childList: true, subtree: true })\n\n // 打开期宿主 childList 观察(不含子树,框架 patch 嵌套组件的 light DOM 不属于\n // 本面板内容)。portal 实例的生命周期即打开期,随恢复/移除断开。\n const migrateAddedNodes = ctx.migrateAddedNodes\n const hostObserver = new MutationObserver(mutations => {\n // 框架清除逻辑(如 lit 的 _$clear)按宿主 marker 邻位遍历已迁移内容:元素\n // 物理上在面板,遍历找不到本体,会把 portal marker 当作内容本体移除。\n // marker 被框架摘除即框架删除语义——解除追踪并清掉滞留面板的游离元素;\n // 同批次移入又移回的 marker(框架重排)不属于删除。实时迁移仍以\n // migrateAddedNodes 的回调为准,无该回调的 portal(如 option 域)只做删除清理。\n const addedNodes = new Set(mutations.flatMap(mutation => [...mutation.addedNodes]))\n const strandedNodes = mutations\n .flatMap(mutation => [...mutation.removedNodes])\n .filter(node => node instanceof Comment && node.textContent === PORTAL_CONTENT_MARKER && !addedNodes.has(node))\n .map(marker => trackedNodes.find(entry => entry.marker === marker)?.node)\n .filter((node): node is Node => node !== undefined)\n if (strandedNodes.length) {\n api.removeContent(strandedNodes)\n // remove() 的规范语义即 parentNode.removeChild:宿主/类型层面都以 Node 表达\n for (const node of strandedNodes) node.parentNode?.removeChild(node)\n }\n if (!migrateAddedNodes) return\n const migratable = migrateAddedNodes(mutations.flatMap(mutation => [...mutation.addedNodes]))\n if (migratable.length) api.appendContent(migratable)\n })\n hostObserver.observe(ctx.target, { childList: true })\n\n const api: OverlayPortal = {\n panel,\n\n moveContent(nodes, target = panel) {\n trackedNodes.forEach(entry => entry.marker.remove())\n const entries = nodes.map(recordEntry).filter((entry): entry is TrackedNodeEntry => entry !== null)\n trackedNodes.length = 0\n trackedNodes.push(...entries)\n target.append(...nodes)\n orderTargetBySkeleton(target)\n },\n\n appendContent(nodes, target = panel) {\n for (const node of nodes) {\n untrackNodes([node])\n const entry = recordEntry(node)\n if (!entry) continue\n target.appendChild(entry.node)\n }\n orderTargetBySkeleton(target)\n },\n\n removeContent(nodes) {\n // 框架删除节点时插进面板的占位注释由 contentObserver 的内部不变量\n // (returnStrayFrameworkComments)即时归还宿主,这里只负责解除追踪。\n untrackNodes(nodes)\n },\n\n restoreContent() {\n // 先停掉实时迁移:恢复动作本身会改动宿主 childList,\n // 否则回插的节点会被当成新增内容再次迁入面板形成回环。\n hostObserver.disconnect()\n\n // 节点回到宿主中自己的 marker 位;面板内紧邻其前的框架注释锚点(v-if 占位)\n // 随元素一起迁回——Vue 仍持有这些锚点的 vnode.el 引用,留在面板会随面板销毁,\n // 下次翻转将因 parentNode === null 崩溃。\n for (const entry of trackedNodes) {\n const pending: Comment[] = []\n let previous = entry.node.previousSibling\n while (previous instanceof Comment && previous.textContent !== PORTAL_CONTENT_MARKER) {\n pending.unshift(previous)\n previous = previous.previousSibling\n }\n for (const comment of pending) {\n if (entry.marker.parentNode === ctx.target) ctx.target.insertBefore(comment, entry.marker)\n else ctx.target.appendChild(comment)\n }\n if (entry.marker.parentNode === ctx.target) ctx.target.insertBefore(entry.node, entry.marker)\n else ctx.target.appendChild(entry.node)\n entry.marker.remove()\n }\n trackedNodes.length = 0\n },\n\n remove() {\n hostObserver.disconnect()\n contentObserver.disconnect()\n // 防御:正常 dispose 先 restore(marker 已清),异常路径下避免 marker 泄漏在宿主。\n trackedNodes.forEach(entry => entry.marker.remove())\n trackedNodes.length = 0\n host.remove()\n }\n }\n return api\n })\n\n/**\n * 判断 node 是否是 slot 已分配节点(或其 light DOM 后代),即 node 实际渲染在 shadowRoot 内。\n * slotted 内容的 parentNode 仍停留在 light DOM,因此不能用普通祖先遍历跨过 shadow 边界,\n * 只能通过 slot.assignedNodes() 判断内容被渲染到哪个 slot。\n */\nfunction isAssignedIntoTarget(node: Node, assignedNode: Node, boundary: Node): boolean {\n let current: Node | null = node\n while (current && current !== boundary) {\n if (current === assignedNode) return true\n current = current.parentNode\n }\n return false\n}\n\n/** parentNode 为 null 时跨过 shadow 边界上行到 host(portal 面板等 shadow 子树的 parentNode 链会断裂)。 */\nfunction getParentAcrossShadow(node: Node): Node | null {\n if (node.parentNode) return node.parentNode\n const root = node.getRootNode()\n return root instanceof ShadowRoot ? root.host : null\n}\n\n/**\n * 查找包含 target 且处于打开态的最近原生 <dialog>。\n * 原生 showModal() 会把 dialog 提升到浏览器 top layer,而常规 overlay 容器在普通文档流,\n * 二者无法靠 z-index 叠序——overlay 会被 top layer 的 dialog 遮住。\n * 因此 target 被渲染到某个已打开的原生 dialog 内时,应把 overlay 挂到该 dialog 上,\n * 使其一并进入 top layer(抽屉 / 对话框内的 dropdown、tooltip、context-menu 均适用)。\n * 上行路径必须跨 shadow 边界:target 可能位于 portal 面板(shadow 子树)内,\n * 纯 parentNode 链在该处断裂会误判为 dialog 外,浮层落进 fallback root 被遮挡。\n */\nfunction findEnclosingOpenDialog(target: Element): HTMLDialogElement | null {\n let current: Node | null = target\n while (current && current !== document.documentElement) {\n if (current instanceof HTMLDialogElement && current.open) return current\n\n // 跨过 shadow host:检查其 shadowRoot 内已打开的 dialog 是否渲染了 target。\n if (current instanceof Element && current.shadowRoot) {\n const dialogs = current.shadowRoot.querySelectorAll('dialog')\n for (const dialog of dialogs) {\n if (!(dialog instanceof HTMLDialogElement) || !dialog.open) continue\n const slots = dialog.querySelectorAll('slot')\n for (const slot of slots) {\n for (const assigned of slot.assignedNodes()) {\n if (!(assigned instanceof Node)) continue\n if (isAssignedIntoTarget(target, assigned, current)) return dialog\n }\n }\n }\n }\n\n current = getParentAcrossShadow(current)\n }\n return null\n}\n\nexport function resolveOverlayContainer(\n container: OverlayContainer | undefined,\n target: Element,\n options: OverlayContainerResolutionOptions = {}\n): HTMLElement {\n const explicit = typeof container === 'function' ? container() : container\n if (explicit) return explicit\n\n // target 在已打开的原生 dialog / drawer 内:挂到该 dialog,加入 top layer。\n const enclosingDialog = findEnclosingOpenDialog(target)\n if (enclosingDialog) return enclosingDialog\n\n // 无 target 的调用(如菜单)优先使用 root theme;有 target 时使用最近的 theme。\n const theme = options.preferRootTheme ? findRootTheme() : findNearestTheme(target)\n return theme?.getOverlayRoot() ?? getFallbackOverlayRoot()\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greypan/web-ui",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Lit-based web components for React, Vue, and vanilla JS",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "dependencies": {
43
43
  "@floating-ui/dom": "^1.8.0",
44
44
  "@lit/context": "^1.1.6",
45
- "@greypan/browser-kit": "2.1.0",
45
+ "@greypan/browser-kit": "2.2.0",
46
46
  "@greypan/js-kit": "2.0.0"
47
47
  },
48
48
  "devDependencies": {
@@ -55,7 +55,10 @@
55
55
  "@iconify-json/radix-icons": "^1.2.6",
56
56
  "@iconify/types": "^2.0.0",
57
57
  "@types/react": "^19.2.17",
58
+ "@types/react-dom": "^19.2.5",
58
59
  "lit": "^3.3.3",
60
+ "react": "^19.2.7",
61
+ "react-dom": "^19.2.7",
59
62
  "vite-plugin-dts": "^5.0.3",
60
63
  "vue": "^3.5.39",
61
64
  "vite": "npm:@voidzero-dev/vite-plus-core@0.3.0",