@greypan/web-ui 5.0.0 → 6.1.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.
- package/README.CN.md +97 -23
- package/README.md +112 -21
- package/dist/assets/glass.js +1 -1
- package/dist/assets/glass.js.map +1 -1
- package/dist/assets/menu-portal.js +1 -1
- package/dist/assets/menu-portal.js.map +1 -1
- package/dist/components/autocomplete/index.d.ts +367 -4
- package/dist/components/autocomplete/index.js +83 -77
- package/dist/components/autocomplete/index.js.map +1 -1
- package/dist/components/autocomplete/style.js +1 -1
- package/dist/components/autocomplete/style.js.map +1 -1
- package/dist/components/avatar/style.js +1 -1
- package/dist/components/avatar/style.js.map +1 -1
- package/dist/components/back-top/style.js +1 -1
- package/dist/components/back-top/style.js.map +1 -1
- package/dist/components/badge/style.js +1 -1
- package/dist/components/badge/style.js.map +1 -1
- package/dist/components/button/style.js +1 -1
- package/dist/components/button/style.js.map +1 -1
- package/dist/components/button-group/style.js +1 -1
- package/dist/components/button-group/style.js.map +1 -1
- package/dist/components/checkbox/index.d.ts +366 -5
- package/dist/components/checkbox/index.js +19 -31
- package/dist/components/checkbox/index.js.map +1 -1
- package/dist/components/checkbox/style.js +1 -1
- package/dist/components/checkbox/style.js.map +1 -1
- package/dist/components/checkbox-group/index.d.ts +366 -4
- package/dist/components/checkbox-group/index.js +16 -25
- package/dist/components/checkbox-group/index.js.map +1 -1
- package/dist/components/collapse/index.js +24 -27
- package/dist/components/collapse/index.js.map +1 -1
- package/dist/components/context-menu/index.d.ts +6 -34
- package/dist/components/context-menu/index.js +88 -201
- package/dist/components/context-menu/index.js.map +1 -1
- package/dist/components/dialog/index.js +20 -23
- package/dist/components/dialog/index.js.map +1 -1
- package/dist/components/dialog/style.js +1 -1
- package/dist/components/dialog/style.js.map +1 -1
- package/dist/components/drawer/index.js +54 -57
- package/dist/components/drawer/index.js.map +1 -1
- package/dist/components/drawer/style.js +1 -1
- package/dist/components/drawer/style.js.map +1 -1
- package/dist/components/dropdown/index.d.ts +11 -14
- package/dist/components/dropdown/index.js +128 -155
- package/dist/components/dropdown/index.js.map +1 -1
- package/dist/components/dropdown-header/style.js +1 -1
- package/dist/components/dropdown-header/style.js.map +1 -1
- package/dist/components/dropdown-item/style.js +1 -1
- package/dist/components/dropdown-item/style.js.map +1 -1
- package/dist/components/empty/style.js +1 -1
- package/dist/components/empty/style.js.map +1 -1
- package/dist/components/input/index.d.ts +366 -5
- package/dist/components/input/index.js +35 -47
- package/dist/components/input/index.js.map +1 -1
- package/dist/components/input/style.js +1 -1
- package/dist/components/input/style.js.map +1 -1
- package/dist/components/input-number/index.d.ts +366 -5
- package/dist/components/input-number/index.js +31 -44
- package/dist/components/input-number/index.js.map +1 -1
- package/dist/components/input-number/style.js +1 -1
- package/dist/components/input-number/style.js.map +1 -1
- package/dist/components/layout/style.js +1 -1
- package/dist/components/layout/style.js.map +1 -1
- package/dist/components/option/style.js +1 -1
- package/dist/components/option/style.js.map +1 -1
- package/dist/components/popover/index.d.ts +2 -0
- package/dist/components/popover/index.js +33 -44
- package/dist/components/popover/index.js.map +1 -1
- package/dist/components/popover/style.js +1 -1
- package/dist/components/popover/style.js.map +1 -1
- package/dist/components/radio/index.d.ts +366 -5
- package/dist/components/radio/index.js +19 -31
- package/dist/components/radio/index.js.map +1 -1
- package/dist/components/radio/style.js +1 -1
- package/dist/components/radio/style.js.map +1 -1
- package/dist/components/radio-group/index.d.ts +366 -4
- package/dist/components/radio-group/index.js +16 -25
- package/dist/components/radio-group/index.js.map +1 -1
- package/dist/components/segmented/index.d.ts +366 -4
- package/dist/components/segmented/index.js +26 -35
- package/dist/components/segmented/index.js.map +1 -1
- package/dist/components/segmented/style.js +1 -1
- package/dist/components/segmented/style.js.map +1 -1
- package/dist/components/segmented-trigger/style.js +1 -1
- package/dist/components/segmented-trigger/style.js.map +1 -1
- package/dist/components/select/index.d.ts +368 -5
- package/dist/components/select/index.js +65 -67
- package/dist/components/select/index.js.map +1 -1
- package/dist/components/select/style.js +1 -1
- package/dist/components/select/style.js.map +1 -1
- package/dist/components/slider/index.d.ts +366 -5
- package/dist/components/slider/index.js +38 -50
- package/dist/components/slider/index.js.map +1 -1
- package/dist/components/slider/style.js +1 -1
- package/dist/components/slider/style.js.map +1 -1
- package/dist/components/spinner/style.js +1 -1
- package/dist/components/spinner/style.js.map +1 -1
- package/dist/components/switch/index.d.ts +366 -5
- package/dist/components/switch/index.js +26 -38
- package/dist/components/switch/index.js.map +1 -1
- package/dist/components/switch/style.js +1 -1
- package/dist/components/switch/style.js.map +1 -1
- package/dist/components/textarea/index.d.ts +366 -5
- package/dist/components/textarea/index.js +43 -57
- package/dist/components/textarea/index.js.map +1 -1
- package/dist/components/textarea/style.js +1 -1
- package/dist/components/textarea/style.js.map +1 -1
- package/dist/components/theme/style.js +1 -1
- package/dist/components/theme/style.js.map +1 -1
- package/dist/components/toast/style.js +1 -1
- package/dist/components/toast/style.js.map +1 -1
- package/dist/components/tooltip/index.d.ts +1 -0
- package/dist/components/tooltip/index.js +32 -46
- package/dist/components/tooltip/index.js.map +1 -1
- package/dist/components/tooltip/style.js +1 -1
- package/dist/components/tooltip/style.js.map +1 -1
- package/dist/shared/form-association/index.js +31 -4
- package/dist/shared/form-association/index.js.map +1 -1
- package/dist/shared/menu-behavior/index.js +112 -0
- package/dist/shared/menu-behavior/index.js.map +1 -0
- package/dist/shared/menu-portal/menu-tree.js +60 -1
- package/dist/shared/menu-portal/menu-tree.js.map +1 -1
- package/dist/shared/open-state/index.js +12 -0
- package/dist/shared/open-state/index.js.map +1 -0
- package/dist/shared/option-portal/combobox-shell.js +41 -0
- package/dist/shared/option-portal/combobox-shell.js.map +1 -0
- package/dist/shared/option-portal/index.js +23 -22
- package/dist/shared/option-portal/index.js.map +1 -1
- package/dist/shared/overlay/overlay.js.map +1 -1
- package/dist/shared/overlay/placement-props.js +19 -0
- package/dist/shared/overlay/placement-props.js.map +1 -0
- package/dist/shared/overlay/portal.js +94 -20
- package/dist/shared/overlay/portal.js.map +1 -1
- package/package.json +6 -7
|
@@ -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":"
|
|
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 })\nexport * from './combobox-shell'\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
|
|
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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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/shared/overlay/placement-props.ts
|
|
2
|
+
var e = [
|
|
3
|
+
"top",
|
|
4
|
+
"top-start",
|
|
5
|
+
"top-end",
|
|
6
|
+
"bottom",
|
|
7
|
+
"bottom-start",
|
|
8
|
+
"bottom-end",
|
|
9
|
+
"left",
|
|
10
|
+
"left-start",
|
|
11
|
+
"left-end",
|
|
12
|
+
"right",
|
|
13
|
+
"right-start",
|
|
14
|
+
"right-end"
|
|
15
|
+
];
|
|
16
|
+
//#endregion
|
|
17
|
+
export { e as FLOATING_PLACEMENTS };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=placement-props.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placement-props.js","names":[],"sources":["../../../src/shared/overlay/placement-props.ts"],"sourcesContent":["/**\n * Floating UI anchored overlay 组件共享的 placement 白名单。\n *\n * 12 个方向成员相同、声明顺序在各组件历史上有差异;normalizeLiteral 只做\n * 成员判定,顺序不参与语义,因此这里以稳定顺序单源化,组件不再各自声明。\n */\nexport const FLOATING_PLACEMENTS = [\n 'top',\n 'top-start',\n 'top-end',\n 'bottom',\n 'bottom-start',\n 'bottom-end',\n 'left',\n 'left-start',\n 'left-end',\n 'right',\n 'right-start',\n 'right-end'\n] as const\n\nexport type FloatingPlacement = (typeof FLOATING_PLACEMENTS)[number]\n"],"mappings":";AAMA,IAAa,IAAsB;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
|
|
@@ -9,42 +9,111 @@ 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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
let o = [], s = new MutationObserver((t) => {
|
|
20
|
+
f(t);
|
|
21
|
+
let n = new Set(t.flatMap((e) => [...e.addedNodes])), r = t.flatMap((e) => [...e.removedNodes]).filter((e) => !n.has(e));
|
|
22
|
+
r.length && l(r), e.onContentChange?.(t);
|
|
23
|
+
}), c = (e) => {
|
|
24
|
+
let t = e.parentNode;
|
|
25
|
+
if (!t) return null;
|
|
26
|
+
let n = document.createComment(a);
|
|
27
|
+
t.insertBefore(n, e);
|
|
28
|
+
let r = {
|
|
29
|
+
node: e,
|
|
30
|
+
marker: n
|
|
31
|
+
};
|
|
32
|
+
return o.push(r), r;
|
|
33
|
+
}, l = (e) => {
|
|
34
|
+
for (let t of e) {
|
|
35
|
+
let e = o.findIndex((e) => e.node === t);
|
|
36
|
+
e >= 0 && (o[e].marker.remove(), o.splice(e, 1));
|
|
37
|
+
}
|
|
38
|
+
}, d = (t) => {
|
|
39
|
+
let n = [];
|
|
40
|
+
for (let r of Array.from(e.target.childNodes)) if (r instanceof Comment && r.textContent === a) {
|
|
41
|
+
let e = o.find((e) => e.marker === r);
|
|
42
|
+
e && e.node.parentNode === t && n.push(e.node);
|
|
43
|
+
}
|
|
44
|
+
if (n.length === 0) return;
|
|
45
|
+
let r = Array.from(t.childNodes).filter((e) => n.includes(e) || !(e instanceof Comment));
|
|
46
|
+
if (!(r.length === n.length && r.every((e, t) => e === n[t]))) for (let e = n.length - 1; e >= 0; e--) {
|
|
47
|
+
let r = n[e], i = n[e + 1];
|
|
48
|
+
i ? r.nextElementSibling !== i && t.insertBefore(r, i) : t.appendChild(r);
|
|
49
|
+
}
|
|
50
|
+
}, f = (t) => {
|
|
51
|
+
let n = (e) => {
|
|
52
|
+
let t = e;
|
|
53
|
+
for (; t && t !== i;) {
|
|
54
|
+
if (o.some((e) => e.node === t)) return !0;
|
|
55
|
+
t = t.parentNode;
|
|
56
|
+
}
|
|
57
|
+
return !1;
|
|
58
|
+
}, r = new Set(t.flatMap((e) => [...e.addedNodes])), s = [];
|
|
59
|
+
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));
|
|
60
|
+
if (!s.length) return;
|
|
61
|
+
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));
|
|
62
|
+
for (let [t, n] of s.entries()) {
|
|
63
|
+
let r = l[Math.min(t, l.length - 1)]?.marker;
|
|
64
|
+
r && r.parentNode === e.target ? e.target.insertBefore(n, r) : e.target.appendChild(n);
|
|
65
|
+
}
|
|
24
66
|
};
|
|
25
|
-
|
|
67
|
+
u(e.container, e.target).appendChild(t), s.observe(i, {
|
|
26
68
|
childList: !0,
|
|
27
69
|
subtree: !0
|
|
28
|
-
})
|
|
70
|
+
});
|
|
71
|
+
let p = e.migrateAddedNodes, m = new MutationObserver((e) => {
|
|
72
|
+
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);
|
|
73
|
+
if (n.length) {
|
|
74
|
+
h.removeContent(n);
|
|
75
|
+
for (let e of n) e.parentNode?.removeChild(e);
|
|
76
|
+
}
|
|
77
|
+
if (!p) return;
|
|
78
|
+
let r = p(e.flatMap((e) => [...e.addedNodes]));
|
|
79
|
+
r.length && h.appendContent(r);
|
|
80
|
+
});
|
|
81
|
+
m.observe(e.target, { childList: !0 });
|
|
82
|
+
let h = {
|
|
29
83
|
panel: i,
|
|
30
84
|
moveContent(e, t = i) {
|
|
31
|
-
|
|
85
|
+
o.forEach((e) => e.marker.remove());
|
|
86
|
+
let n = e.map(c).filter((e) => e !== null);
|
|
87
|
+
o.length = 0, o.push(...n), t.append(...e), d(t);
|
|
32
88
|
},
|
|
33
89
|
appendContent(e, t = i) {
|
|
34
|
-
|
|
90
|
+
for (let n of e) {
|
|
91
|
+
l([n]);
|
|
92
|
+
let e = c(n);
|
|
93
|
+
e && t.appendChild(e.node);
|
|
94
|
+
}
|
|
95
|
+
d(t);
|
|
35
96
|
},
|
|
36
97
|
removeContent(e) {
|
|
37
|
-
|
|
98
|
+
l(e);
|
|
38
99
|
},
|
|
39
100
|
restoreContent() {
|
|
40
|
-
|
|
101
|
+
m.disconnect();
|
|
102
|
+
for (let t of o) {
|
|
103
|
+
let n = [], r = t.node.previousSibling;
|
|
104
|
+
for (; r instanceof Comment && r.textContent !== a;) n.unshift(r), r = r.previousSibling;
|
|
105
|
+
for (let r of n) t.marker.parentNode === e.target ? e.target.insertBefore(r, t.marker) : e.target.appendChild(r);
|
|
106
|
+
t.marker.parentNode === e.target ? e.target.insertBefore(t.node, t.marker) : e.target.appendChild(t.node), t.marker.remove();
|
|
107
|
+
}
|
|
108
|
+
o.length = 0;
|
|
41
109
|
},
|
|
42
110
|
remove() {
|
|
43
|
-
|
|
111
|
+
m.disconnect(), s.disconnect(), o.forEach((e) => e.marker.remove()), o.length = 0, t.remove();
|
|
44
112
|
}
|
|
45
113
|
};
|
|
114
|
+
return h;
|
|
46
115
|
});
|
|
47
|
-
function
|
|
116
|
+
function s(e, t, n) {
|
|
48
117
|
let r = e;
|
|
49
118
|
for (; r && r !== n;) {
|
|
50
119
|
if (r === t) return !0;
|
|
@@ -52,7 +121,12 @@ function o(e, t, n) {
|
|
|
52
121
|
}
|
|
53
122
|
return !1;
|
|
54
123
|
}
|
|
55
|
-
function
|
|
124
|
+
function c(e) {
|
|
125
|
+
if (e.parentNode) return e.parentNode;
|
|
126
|
+
let t = e.getRootNode();
|
|
127
|
+
return t instanceof ShadowRoot ? t.host : null;
|
|
128
|
+
}
|
|
129
|
+
function l(e) {
|
|
56
130
|
let t = e;
|
|
57
131
|
for (; t && t !== document.documentElement;) {
|
|
58
132
|
if (t instanceof HTMLDialogElement && t.open) return t;
|
|
@@ -61,17 +135,17 @@ function s(e) {
|
|
|
61
135
|
for (let r of n) {
|
|
62
136
|
if (!(r instanceof HTMLDialogElement) || !r.open) continue;
|
|
63
137
|
let n = r.querySelectorAll("slot");
|
|
64
|
-
for (let i of n) for (let n of i.assignedNodes()) if (n instanceof Node &&
|
|
138
|
+
for (let i of n) for (let n of i.assignedNodes()) if (n instanceof Node && s(e, n, t)) return r;
|
|
65
139
|
}
|
|
66
140
|
}
|
|
67
|
-
t = t
|
|
141
|
+
t = c(t);
|
|
68
142
|
}
|
|
69
143
|
return null;
|
|
70
144
|
}
|
|
71
|
-
function
|
|
72
|
-
return (typeof r == "function" ? r() : r) ||
|
|
145
|
+
function u(r, i, a = {}) {
|
|
146
|
+
return (typeof r == "function" ? r() : r) || l(i) || ((a.preferRootTheme ? n() : t(i))?.getOverlayRoot() ?? e());
|
|
73
147
|
}
|
|
74
148
|
//#endregion
|
|
75
|
-
export { i as applyOverlayVariables,
|
|
149
|
+
export { i as applyOverlayVariables, o as defineOverlayPortal, u as resolveOverlayContainer };
|
|
76
150
|
|
|
77
151
|
//# 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 /**\n * 额外的面板内容变化反应。注意:已迁移节点被框架物理删除后的解除追踪\n * (removeContent)是 portal 的内建默认行为,不经过该回调;这里只承载\n * 组件自己的附加逻辑(如 option 域的刷新、空态占位改写)。\n */\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 // 框架在打开期物理删除已迁移节点:即时解除追踪,否则关闭恢复时会把已删除\n // 节点复活回宿主 light DOM。面板内部移动(重排 insertBefore)在同一 mutation\n // record 中同时出现在 removed/added,不属于删除,须排除。\n const addedInBatch = new Set(mutations.flatMap(mutation => [...mutation.addedNodes]))\n const removedNodes = mutations\n .flatMap(mutation => [...mutation.removedNodes])\n .filter(node => !addedInBatch.has(node))\n if (removedNodes.length) untrackNodes(removedNodes)\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 // 面板内容观察是 portal 自身的不变量(游离注释救援 + 删除节点解除追踪),\n // 不依赖消费方是否注册 onContentChange。\n 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": "
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "Lit-based web components for React, Vue, and vanilla JS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,10 +28,6 @@
|
|
|
28
28
|
"types": "./dist/icons/index.d.ts",
|
|
29
29
|
"import": "./dist/icons/index.js"
|
|
30
30
|
},
|
|
31
|
-
"./icons/*": {
|
|
32
|
-
"types": "./dist/icons/*.d.ts",
|
|
33
|
-
"import": "./dist/icons/*.js"
|
|
34
|
-
},
|
|
35
31
|
"./types/*": "./dist/types/*.d.ts",
|
|
36
32
|
"./package.json": "./package.json"
|
|
37
33
|
},
|
|
@@ -42,8 +38,8 @@
|
|
|
42
38
|
"dependencies": {
|
|
43
39
|
"@floating-ui/dom": "^1.8.0",
|
|
44
40
|
"@lit/context": "^1.1.6",
|
|
45
|
-
"@greypan/browser-kit": "
|
|
46
|
-
"@greypan/js-kit": "
|
|
41
|
+
"@greypan/browser-kit": "3.0.0",
|
|
42
|
+
"@greypan/js-kit": "3.0.0"
|
|
47
43
|
},
|
|
48
44
|
"devDependencies": {
|
|
49
45
|
"@iconify-json/jam": "^1.2.3",
|
|
@@ -55,7 +51,10 @@
|
|
|
55
51
|
"@iconify-json/radix-icons": "^1.2.6",
|
|
56
52
|
"@iconify/types": "^2.0.0",
|
|
57
53
|
"@types/react": "^19.2.17",
|
|
54
|
+
"@types/react-dom": "^19.2.5",
|
|
58
55
|
"lit": "^3.3.3",
|
|
56
|
+
"react": "^19.2.7",
|
|
57
|
+
"react-dom": "^19.2.7",
|
|
59
58
|
"vite-plugin-dts": "^5.0.3",
|
|
60
59
|
"vue": "^3.5.39",
|
|
61
60
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.0",
|