@greypan/web-ui 8.0.0 → 9.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.
Files changed (189) hide show
  1. package/README.CN.md +277 -146
  2. package/README.md +323 -191
  3. package/dist/assets/glass.js +1 -1
  4. package/dist/assets/glass.js.map +1 -1
  5. package/dist/assets/menu-portal.js +1 -1
  6. package/dist/assets/menu-portal.js.map +1 -1
  7. package/dist/assets/selection-control.js +1 -1
  8. package/dist/assets/selection-control.js.map +1 -1
  9. package/dist/assets/toast-containers.js +1 -1
  10. package/dist/assets/toast-containers.js.map +1 -1
  11. package/dist/components/autocomplete/index.d.ts +3 -2
  12. package/dist/components/autocomplete/index.js +46 -50
  13. package/dist/components/autocomplete/index.js.map +1 -1
  14. package/dist/components/autocomplete/style.js +1 -1
  15. package/dist/components/autocomplete/style.js.map +1 -1
  16. package/dist/components/avatar/index.js.map +1 -1
  17. package/dist/components/avatar/style.js +1 -1
  18. package/dist/components/avatar/style.js.map +1 -1
  19. package/dist/components/back-top/style.js +1 -1
  20. package/dist/components/back-top/style.js.map +1 -1
  21. package/dist/components/badge/style.js +1 -1
  22. package/dist/components/badge/style.js.map +1 -1
  23. package/dist/components/button/index.d.ts +368 -1
  24. package/dist/components/button/index.js +56 -36
  25. package/dist/components/button/index.js.map +1 -1
  26. package/dist/components/button/style.js +1 -1
  27. package/dist/components/button/style.js.map +1 -1
  28. package/dist/components/button-group/index.js +0 -1
  29. package/dist/components/button-group/index.js.map +1 -1
  30. package/dist/components/button-group/style.js +1 -1
  31. package/dist/components/button-group/style.js.map +1 -1
  32. package/dist/components/checkbox/index.d.ts +3 -0
  33. package/dist/components/checkbox/index.js +35 -37
  34. package/dist/components/checkbox/index.js.map +1 -1
  35. package/dist/components/checkbox/style.js +1 -1
  36. package/dist/components/checkbox/style.js.map +1 -1
  37. package/dist/components/checkbox-group/index.js +8 -8
  38. package/dist/components/checkbox-group/style.js +1 -1
  39. package/dist/components/checkbox-group/style.js.map +1 -1
  40. package/dist/components/context-menu/index.d.ts +167 -3
  41. package/dist/components/context-menu/index.js +162 -91
  42. package/dist/components/context-menu/index.js.map +1 -1
  43. package/dist/components/context-menu/scrim.js +6 -0
  44. package/dist/components/context-menu/scrim.js.map +1 -0
  45. package/dist/components/context-menu/style.js +1 -1
  46. package/dist/components/context-menu/style.js.map +1 -1
  47. package/dist/components/dialog/index.d.ts +9 -0
  48. package/dist/components/dialog/index.js +61 -46
  49. package/dist/components/dialog/index.js.map +1 -1
  50. package/dist/components/dialog/style.js +1 -1
  51. package/dist/components/dialog/style.js.map +1 -1
  52. package/dist/components/drawer/index.d.ts +45 -0
  53. package/dist/components/drawer/index.js +116 -52
  54. package/dist/components/drawer/index.js.map +1 -1
  55. package/dist/components/drawer/style.js +1 -1
  56. package/dist/components/drawer/style.js.map +1 -1
  57. package/dist/components/dropdown/index.d.ts +12 -0
  58. package/dist/components/dropdown/index.js +13 -10
  59. package/dist/components/dropdown/index.js.map +1 -1
  60. package/dist/components/dropdown-divider/style.js +1 -1
  61. package/dist/components/dropdown-divider/style.js.map +1 -1
  62. package/dist/components/dropdown-header/style.js +1 -1
  63. package/dist/components/dropdown-header/style.js.map +1 -1
  64. package/dist/components/dropdown-item/style.js +1 -1
  65. package/dist/components/dropdown-item/style.js.map +1 -1
  66. package/dist/components/editable-text/index.d.ts +41 -0
  67. package/dist/components/editable-text/index.js +55 -31
  68. package/dist/components/editable-text/index.js.map +1 -1
  69. package/dist/components/empty/index.d.ts +21 -4
  70. package/dist/components/empty/index.js +37 -32
  71. package/dist/components/empty/index.js.map +1 -1
  72. package/dist/components/empty/style.js +1 -1
  73. package/dist/components/empty/style.js.map +1 -1
  74. package/dist/components/icon/index.js +7 -7
  75. package/dist/components/icon/index.js.map +1 -1
  76. package/dist/components/image-preview/index.js +9 -2
  77. package/dist/components/image-preview/index.js.map +1 -1
  78. package/dist/components/image-preview/style.js +1 -1
  79. package/dist/components/image-preview/style.js.map +1 -1
  80. package/dist/components/index.js +23 -23
  81. package/dist/components/input/index.d.ts +7 -0
  82. package/dist/components/input/index.js +34 -29
  83. package/dist/components/input/index.js.map +1 -1
  84. package/dist/components/input/style.js +1 -1
  85. package/dist/components/input/style.js.map +1 -1
  86. package/dist/components/input-number/index.d.ts +1 -0
  87. package/dist/components/input-number/index.js +28 -26
  88. package/dist/components/input-number/index.js.map +1 -1
  89. package/dist/components/input-number/style.js +1 -1
  90. package/dist/components/input-number/style.js.map +1 -1
  91. package/dist/components/layout/index.d.ts +0 -3
  92. package/dist/components/layout/index.js +1 -1
  93. package/dist/components/layout/index.js.map +1 -1
  94. package/dist/components/layout/style.js +1 -1
  95. package/dist/components/layout/style.js.map +1 -1
  96. package/dist/components/option/style.js +1 -1
  97. package/dist/components/option/style.js.map +1 -1
  98. package/dist/components/popover/index.js.map +1 -1
  99. package/dist/components/popover/style.js +1 -1
  100. package/dist/components/popover/style.js.map +1 -1
  101. package/dist/components/radio/index.d.ts +3 -0
  102. package/dist/components/radio/index.js +23 -19
  103. package/dist/components/radio/index.js.map +1 -1
  104. package/dist/components/radio/style.js +1 -1
  105. package/dist/components/radio/style.js.map +1 -1
  106. package/dist/components/radio-group/index.js +8 -8
  107. package/dist/components/radio-group/style.js +1 -1
  108. package/dist/components/radio-group/style.js.map +1 -1
  109. package/dist/components/segmented/index.d.ts +5 -0
  110. package/dist/components/segmented/index.js +49 -32
  111. package/dist/components/segmented/index.js.map +1 -1
  112. package/dist/components/segmented/style.js +1 -1
  113. package/dist/components/segmented/style.js.map +1 -1
  114. package/dist/components/segmented-trigger/style.js +1 -1
  115. package/dist/components/segmented-trigger/style.js.map +1 -1
  116. package/dist/components/select/index.js +0 -1
  117. package/dist/components/select/index.js.map +1 -1
  118. package/dist/components/select/style.js +1 -1
  119. package/dist/components/select/style.js.map +1 -1
  120. package/dist/components/slider/index.js +9 -9
  121. package/dist/components/slider/style.js +1 -1
  122. package/dist/components/slider/style.js.map +1 -1
  123. package/dist/components/spinner/style.js +1 -1
  124. package/dist/components/spinner/style.js.map +1 -1
  125. package/dist/components/svg-draw-lines/index.js.map +1 -1
  126. package/dist/components/switch/index.d.ts +1 -0
  127. package/dist/components/switch/index.js +26 -24
  128. package/dist/components/switch/index.js.map +1 -1
  129. package/dist/components/switch/style.js +1 -1
  130. package/dist/components/switch/style.js.map +1 -1
  131. package/dist/components/textarea/index.d.ts +7 -1
  132. package/dist/components/textarea/index.js +45 -43
  133. package/dist/components/textarea/index.js.map +1 -1
  134. package/dist/components/textarea/style.js +1 -1
  135. package/dist/components/textarea/style.js.map +1 -1
  136. package/dist/components/theme/index.d.ts +9 -0
  137. package/dist/components/theme/index.js +128 -85
  138. package/dist/components/theme/index.js.map +1 -1
  139. package/dist/components/theme/style.js +1 -1
  140. package/dist/components/theme/style.js.map +1 -1
  141. package/dist/components/toast/style.js +1 -1
  142. package/dist/components/toast/style.js.map +1 -1
  143. package/dist/components/toast/toast.js +8 -8
  144. package/dist/components/toast/toast.js.map +1 -1
  145. package/dist/components/tooltip/index.js.map +1 -1
  146. package/dist/components/tooltip/style.js +1 -1
  147. package/dist/components/tooltip/style.js.map +1 -1
  148. package/dist/icons/generated/akar-icons-check.d.ts +6 -0
  149. package/dist/icons/generated/akar-icons-check.js +10 -0
  150. package/dist/icons/generated/akar-icons-check.js.map +1 -0
  151. package/dist/icons/generated/akar-icons-circle-check.d.ts +6 -0
  152. package/dist/icons/generated/akar-icons-circle-check.js +10 -0
  153. package/dist/icons/generated/akar-icons-circle-check.js.map +1 -0
  154. package/dist/icons/generated/bi-check-lg.d.ts +6 -0
  155. package/dist/icons/generated/bi-check-lg.js +10 -0
  156. package/dist/icons/generated/bi-check-lg.js.map +1 -0
  157. package/dist/icons/generated/bi-check.d.ts +6 -0
  158. package/dist/icons/generated/bi-check.js +10 -0
  159. package/dist/icons/generated/bi-check.js.map +1 -0
  160. package/dist/icons/generated/fluent-tag-search-24-regular.d.ts +6 -0
  161. package/dist/icons/generated/fluent-tag-search-24-regular.js +10 -0
  162. package/dist/icons/generated/fluent-tag-search-24-regular.js.map +1 -0
  163. package/dist/icons/generated/mdi-tag-search-outline.d.ts +6 -0
  164. package/dist/icons/generated/mdi-tag-search-outline.js +10 -0
  165. package/dist/icons/generated/mdi-tag-search-outline.js.map +1 -0
  166. package/dist/icons/index.d.ts +6 -0
  167. package/dist/icons/index.js +7 -1
  168. package/dist/shared/element-height/index.js +47 -0
  169. package/dist/shared/element-height/index.js.map +1 -0
  170. package/dist/shared/form-association/field-id.js +9 -0
  171. package/dist/shared/form-association/field-id.js.map +1 -0
  172. package/dist/shared/form-association/index.js +9 -1
  173. package/dist/shared/form-association/index.js.map +1 -1
  174. package/dist/shared/label-emptiness/index.js +45 -0
  175. package/dist/shared/label-emptiness/index.js.map +1 -0
  176. package/dist/shared/menu-portal/menu-portal.js +11 -11
  177. package/dist/shared/menu-portal/menu-portal.js.map +1 -1
  178. package/dist/shared/menu-portal/menu-tree.js +2 -2
  179. package/dist/shared/menu-portal/menu-tree.js.map +1 -1
  180. package/dist/shared/overlay/anchored-panel.js.map +1 -1
  181. package/dist/shared/overlay/native-dialog-presence.js +23 -11
  182. package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
  183. package/dist/shared/overlay/nested-drawer-layers.js +30 -23
  184. package/dist/shared/overlay/nested-drawer-layers.js.map +1 -1
  185. package/dist/shared/scroll-lock/scroll-lock.js +3 -3
  186. package/dist/shared/scroll-lock/scroll-lock.js.map +1 -1
  187. package/dist/shared/theme/root-sync.js +24 -18
  188. package/dist/shared/theme/root-sync.js.map +1 -1
  189. package/package.json +8 -4
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/dropdown/index.ts"],"sourcesContent":["import type { Placement } from '@floating-ui/dom'\nimport { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport '@/components/dropdown-divider'\nimport '@/components/dropdown-header'\nimport '@/components/dropdown-item'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport {\n createClosingSubmenuStack,\n createMenuHoverBinder,\n createMenuOutsideClickGuard,\n getEnabledMenuLevelItems,\n getFocusedMenuItemFromPanels,\n handleMenuKeyboard,\n type MenuHoverDelegate,\n type MenuKeyboardDelegate\n} from '@/shared/menu-behavior'\nimport { createMenuPortalOverlay } from '@/shared/menu-portal/menu-portal'\nimport {\n captureFrameworkAnchors,\n focusMenuItem,\n getMenuChildren,\n getMenuItemFromEvent,\n hideNestedMenuChildren,\n moveMenuChildren,\n orderManagedMenuItems,\n reconcileManagedMenuItems,\n restoreFrameworkAnchors,\n returnManagedMenuItemsToSlot,\n type MenuItemAnchors\n} from '@/shared/menu-portal/menu-tree'\nimport { normalizeLiteral, normalizeNumber } from '@/shared/normalize'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { defineOpenOverlay, type OpenOverlayHandle } from '@/shared/overlay/open-overlay'\nimport { defineOverlay } from '@/shared/overlay/overlay'\nimport type { OverlayApi } from '@/shared/overlay/overlay'\nimport { FLOATING_PLACEMENTS } from '@/shared/overlay/placement-props'\nimport { hideOverlayPresence, showOverlayPresence } from '@/shared/overlay/presence'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\n\nimport style from './style.css?inline'\n\nexport type { Placement }\n\nconst SLOT_PREFIX = 'web-ui-menu-level-'\n// 一级菜单项的模板位 marker(机制与 context-menu 共用,见 menu-tree 的托管项锚说明)。\nconst MENU_ITEM_MARKER = 'wui-dropdown-menu-item'\n\nlet dropdownIdCounter = 0\n\ninterface MenuOverlay {\n api: OverlayApi\n overlay: HTMLElement\n content: HTMLElement\n /** 该层为子菜单时的会话句柄;根层句柄由组件单独持有(见 `_handle`)。 */\n handle?: OpenOverlayHandle\n}\n\n@customElement('web-ui-dropdown')\nexport class WebUiDropdown extends LitElement {\n static override styles = unsafeCSS(style)\n\n @property({ type: Boolean, reflect: true }) open = false\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true, attribute: 'match-width' }) matchWidth = false\n @property({ type: Boolean, reflect: true, attribute: 'no-scroll-lock' }) noScrollLock = false\n\n private _placement: Placement = 'bottom-start'\n\n @property({ type: String, reflect: true })\n get placement(): Placement {\n return this._placement\n }\n set placement(v: string) {\n this._placement = normalizeLiteral(v, FLOATING_PLACEMENTS, 'bottom-start')\n }\n\n private _offset = 4\n\n @property({ type: Number })\n get offset(): number {\n return this._offset\n }\n set offset(v: number) {\n this._offset = normalizeNumber(v, 0, 100, 4)\n }\n\n @state() private _activePath: number[] = []\n\n private readonly _overlays = new Map<number, MenuOverlay>()\n /** 实例作用域的帧事务入口(合并了原 lifecycle)。 */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => this._keyboardDelegate.closeDeepestOrAll(),\n isConnected: () => this.isConnected\n })\n private readonly _scrollLock = defineScrollLockLease().make()\n /**\n * 当前 Escape 候选的会话句柄(level 0)。面板要等一帧才建好,而 Escape 可能在这之前\n * 到达:先以宿主为 panel claim 一次,面板就绪后 release 旧会话、再以真实面板重新 claim。\n * 子菜单的句柄存在各自 `MenuOverlay.handle` 上(经 adopt 进本句柄子树)。\n */\n private _handle: OpenOverlayHandle | null = null\n // 行为层(hover / outside-click / 键盘 / submenu 收尾)由 shared/menu-behavior 驱动\n private readonly _outsideClickGuard = createMenuOutsideClickGuard(\n this,\n // 根句柄的 contains 递归覆盖全部已 adopt 的子层,一次查询替代逐层判定。\n node => this._handle?.contains(node) ?? false\n )\n private readonly _closingSubmenus = createClosingSubmenuStack<MenuOverlay>({\n getPanel: container => container.overlay,\n restoreItems: (container, parentItem) => moveMenuChildren(container.content, parentItem),\n dispose: container => {\n container.handle?.release()\n container.overlay.remove()\n container.api.dispose()\n }\n })\n private readonly _hoverDelegate: MenuHoverDelegate = {\n getOpenDepth: () => this._activePath.length,\n getLevelItems: level => this._getLevelItems(level),\n getActiveItem: level => this._getLevelItems(level)[this._activePath[level]],\n isSubmenuItem: item => item.hasAttribute('submenu'),\n openSubmenu: item => {\n const level = this._getFocusedLevel(item)\n if (level === undefined) return\n const itemIndex = this._getLevelItems(level).indexOf(item)\n if (itemIndex >= 0) this._toggleSubmenu(level, itemIndex)\n },\n closeFrom: level => this._closeSubmenuFrom(level + 1)\n }\n private readonly _hoverBinder = createMenuHoverBinder(this._hoverDelegate, 'web-ui-dropdown-item')\n private readonly _keyboardDelegate: MenuKeyboardDelegate = {\n getFocusedItem: event =>\n getFocusedMenuItemFromPanels(\n event,\n [...this._overlays.values()].map(({ overlay }) => overlay),\n 'web-ui-dropdown-item'\n ),\n getLevelOf: item => this._getFocusedLevel(item),\n getEnabledItems: level => getEnabledMenuLevelItems(this._overlays.get(level)?.content),\n isSubmenuItem: item => item.hasAttribute('submenu'),\n openSubmenuInstant: item => {\n const level = this._getFocusedLevel(item)\n if (level === undefined) return\n const itemIndex = this._getLevelItems(level).indexOf(item)\n if (itemIndex >= 0) this._toggleSubmenu(level, itemIndex, true)\n },\n closeToParent: level => {\n this._closeSubmenuFrom(level)\n return this._getLevelItems(level - 1)[this._activePath[level - 1]]\n },\n closeDeepestOrAll: () => {\n if (this._activePath.length > 0) this._closeSubmenuFrom(this._activePath.length)\n else this._closeAll(true)\n }\n }\n private readonly _userOpenChange = new UserChangeController()\n private _restoreFocusTarget?: HTMLElement\n private _shouldOpenInstantly = true\n private _menuSyncScheduled = false\n private readonly _level0ItemAnchors: MenuItemAnchors = new Map()\n // 打开期实时渲染:框架(Vue/React 条件渲染)可能在菜单打开期间持续插入菜单项。\n // 观察宿主子树(新增项可能被框架 wrapper 包裹)与各层级面板 content 的子树\n // (打开中的 submenu 父项已被迁入 content,其新增子项只在这里可见),下一帧统一\n // 把新子树迁入对应层级面板;面板尺寸变化由 defineOverlay 的 autoUpdate 跟进重定位。\n private readonly _menuContentObserver = new MutationObserver(() => {\n if (this.open) this._scheduleMenuSync()\n })\n\n get isOpen(): boolean {\n return this.open || this._activePath.length > 0\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._overlay.resume()\n this.addEventListener('keydown', this._onKeydown)\n document.addEventListener('click', this._onClickOutside)\n this._menuContentObserver.observe(this, { childList: true, subtree: true })\n /*\n * 重挂载对账:`disconnectedCallback` 会拆掉面板、把菜单项迁回宿主,但 `open` 是公开\n * prop,不因卸载而改写。Lit 在 detach 期间不记 `changedProperties`,重连后 `updated()`\n * 不再命中 `changed.has('open')`,面板于是永久缺席:宿主仍反射 `open` 与\n * `aria-expanded`,Escape 和 outside click 却无层可关。首连时 `open` 在\n * `changedProperties` 里、由 `updated()` 负责,`hasUpdated` 就是用来只认重连的。\n */\n if (this.open && this.hasUpdated) this._syncOpenOverlay(this._isFocusOrphaned())\n }\n\n override firstUpdated() {\n // trigger slot 内容增删不触发宿主响应式更新:显式请求一轮渲染,\n // 由 updated() 的 ARIA 回写覆盖晚到的 trigger 元素。\n this.shadowRoot\n ?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n ?.addEventListener('slotchange', () => this.requestUpdate())\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._menuContentObserver.disconnect()\n this.removeEventListener('keydown', this._onKeydown)\n document.removeEventListener('click', this._onClickOutside)\n this._outsideClickGuard.dispose()\n this._hoverBinder.dispose()\n // 停止帧调度;登记由 _cleanupClosedMenu → _disposeAll 撤销。\n this._overlay.suspend()\n this._scrollLock.release()\n this._cleanupClosedMenu()\n }\n\n protected override updated(changed: Map<string, unknown>) {\n // ARIA 回写不依赖 open 分支,任何渲染后都保持与宿主状态同步。\n this._syncTriggerAria()\n\n if (changed.has('open')) {\n if (this.open) {\n this._syncOpenOverlay(true)\n } else {\n this._overlay.invalidate()\n this._syncScrollLock()\n /*\n * release ⟺ 关闭:新模块里「已登记」就等于「参与仲裁」,不再由 host.isOpen()\n * 兜底过滤,所以必须在这里撤销,不能等到退场动画结束。\n */\n this._handle?.release()\n this._handle = null\n void this._closeRootAfterPresence()\n }\n if (this._userOpenChange.consume()) {\n dispatchOpenChangeEvent(this, this.open)\n }\n }\n if (changed.has('noScrollLock')) this._syncScrollLock()\n if (changed.has('placement') || changed.has('offset') || changed.has('matchWidth')) {\n this._overlays.get(0)?.api.update({\n placement: this.placement,\n offset: this.offset,\n matchWidth: this.matchWidth\n })\n }\n this._hoverBinder.bind()\n }\n\n /**\n * 焦点是否已无归属:被 detach 打掉的焦点会落回 `body`,用户主动点到别处时不会。\n * 重挂载对账只在前者才把焦点夺回菜单,否则等于从消费者手里抢焦点。\n */\n private _isFocusOrphaned(): boolean {\n const active = document.activeElement\n return active === null || active === document.body || active === document.documentElement\n }\n\n /**\n * 打开态的唯一对账入口:面板、滚动锁、outside click、hover 与菜单焦点都从这里收敛,\n * `updated()` 的打开分支与重连对账共用它,两条路径必须落到同一个状态。\n */\n private _syncOpenOverlay(takeFocus: boolean) {\n this._restoreFocusTarget ??= document.activeElement instanceof HTMLElement ? document.activeElement : undefined\n this._outsideClickGuard.arm()\n this._syncScrollLock()\n this._hideAllSubmenuChildren()\n this._overlay.invalidate()\n this._overlay.scheduleFrame(() => {\n this._ensureOverlay(0, this._shouldOpenInstantly)\n this._shouldOpenInstantly = true\n if (takeFocus) focusMenuItem(getEnabledMenuLevelItems(this._overlays.get(0)?.content)[0])\n // 面板 id 在 overlay 构建后才有:回写 aria-controls 指向。\n this._syncTriggerAria()\n })\n this._bindHoversAfterUpdate()\n }\n\n // 命令式打开,不派发 open-change。\n openMenu() {\n this._openMenu(true, false)\n }\n\n private _openMenu(isInstant: boolean, fromUser: boolean) {\n if (this.disabled || this.open) return\n this._outsideClickGuard.arm()\n this._shouldOpenInstantly = isInstant\n if (fromUser) this._userOpenChange.mark()\n // 面板要等一帧才建好,而 Escape 可能在这之前到达:先用宿主当 panel claim 一次;\n // 面板就绪后 _buildOverlay 会 release 这个占位会话,再以真实面板重新 claim。\n this._claimRoot(this._overlays.get(0)?.overlay ?? this)\n this.open = true\n }\n\n // 命令式关闭全部层级,不派发 open-change。\n closeAll() {\n this._closeAll(false)\n }\n\n private _closeAll(fromUser: boolean) {\n if (!this.open) return\n if (fromUser) this._userOpenChange.mark()\n this.open = false\n }\n\n // 不影响 open prop。\n private _closeAllSubmenus() {\n this._closeSubmenuFrom(1, true)\n this._closingSubmenus.restoreAll()\n this._syncActiveAttrs()\n }\n\n private _cleanupClosedMenu() {\n this._closeAllSubmenus()\n this._returnLevel0Items()\n this._disposeAll()\n }\n\n private async _closeRootAfterPresence() {\n const overlay = this._overlays.get(0)?.overlay\n if (overlay && !(await hideOverlayPresence(overlay))) return\n if (this.open || !this.isConnected) return\n\n this._cleanupClosedMenu()\n this._restoreFocusTarget?.focus()\n this._restoreFocusTarget = undefined\n }\n\n private _hideAllSubmenuChildren() {\n hideNestedMenuChildren(this, `${SLOT_PREFIX}-hidden`)\n }\n\n private _toggleSubmenu(level: number, itemIndex: number, isInstant = false) {\n if (this._activePath[level] === itemIndex) {\n return\n }\n\n this._closeSubmenuFrom(level + 1)\n\n this._activePath = [...this._activePath.slice(0, level), itemIndex]\n this._syncActiveAttrs()\n\n const item = this._getLevelItems(level)[itemIndex]\n if (item?.hasAttribute('submenu')) {\n this._overlay.scheduleFrame(() => {\n if (!this.open || this._activePath[level] !== itemIndex) return\n const restored = this._ensureOverlay(level + 1, isInstant, item)\n if (!restored) this._populateOverlay(level + 1, item)\n })\n }\n\n this._bindHoversAfterUpdate()\n }\n\n private _closeSubmenuFrom(level: number, isInstant = false) {\n for (let lv = this._activePath.length; lv >= level; lv--) {\n const overlay = this._overlays.get(lv)\n const parentItem = this._getLevelItems(lv - 1)[this._activePath[lv - 1]]\n if (!overlay) continue\n\n if (!parentItem || isInstant) {\n this._depopulateOverlay(lv, parentItem)\n this._disposeOverlay(lv)\n } else {\n this._overlays.delete(lv)\n this._closingSubmenus.closeAsync(parentItem, overlay)\n }\n }\n const nextActivePath = this._activePath.slice(0, level - 1)\n if (nextActivePath.length !== this._activePath.length) {\n this._activePath = nextActivePath\n }\n this._syncActiveAttrs()\n }\n\n private _syncActiveAttrs() {\n this.querySelectorAll('web-ui-dropdown-item').forEach(item => {\n item.removeAttribute('active')\n })\n this._overlays.forEach((_, level) => {\n this._getLevelItems(level).forEach(item => item.removeAttribute('active'))\n })\n this._activePath.forEach((itemIndex, level) => {\n const item = this._getLevelItems(level)[itemIndex]\n if (item?.matches('web-ui-dropdown-item')) {\n item.setAttribute('active', '')\n }\n })\n }\n\n private _getLevelItems(level: number): HTMLElement[] {\n const content = this._overlays.get(level)?.content\n return content ? getMenuChildren(content) : []\n }\n\n private _populateLevel0() {\n const content = this._overlays.get(0)?.content\n if (!content) return\n // 初次打开即建立 marker 锚:宿主此后始终持有模板序骨架,打开期框架插入与\n // 关闭归还都以它为基准(机制与 context-menu 共用,见 menu-tree)。\n reconcileManagedMenuItems(this, content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n orderManagedMenuItems(this, content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n }\n\n private _scheduleMenuSync() {\n if (this._menuSyncScheduled) return\n this._menuSyncScheduled = true\n requestAnimationFrame(() => {\n this._menuSyncScheduled = false\n if (!this.open) return\n this._syncOpenMenuContent()\n })\n }\n\n // 打开期的统一内容同步:把打开期间框架新增的菜单子树迁入对应层级面板。\n // 一级项走 marker 锚 reconcile(保序 + 关闭按模板位归还);面板尺寸变化由\n // defineOverlay 的 autoUpdate 跟进重定位。\n private _syncOpenMenuContent() {\n const level0Content = this._overlays.get(0)?.content\n // 迁移前隐藏宿主中新项的嵌套子项(hidden slot 属性随元素一起迁移)。\n this._hideAllSubmenuChildren()\n if (level0Content) {\n reconcileManagedMenuItems(this, level0Content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n // 新迁入项的嵌套子项未打 hidden slot,会经 dropdown-item 默认 slot 渲染进\n // 父项 label 叠到菜单上;面板项在面板内渲染,宿主查询够不到,须对 content 补隐藏。\n hideNestedMenuChildren(level0Content, `${SLOT_PREFIX}-hidden`)\n // 框架注释锚点复位独立于元素重排:锚点漂移不收敛会破坏后续翻转的插入点(同 context-menu)。\n const frameworkAnchors = captureFrameworkAnchors(level0Content, MENU_ITEM_MARKER)\n orderManagedMenuItems(this, level0Content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n restoreFrameworkAnchors(level0Content, frameworkAnchors)\n }\n for (const [level, overlay] of this._overlays) {\n if (level === 0) continue\n const parentItem = this._getLevelItems(level - 1)[this._activePath[level - 1]]\n if (parentItem) moveMenuChildren(parentItem, overlay.content)\n hideNestedMenuChildren(overlay.content, `${SLOT_PREFIX}-hidden`)\n }\n this._hoverBinder.bind()\n }\n\n private _returnLevel0Items() {\n const content = this._overlays.get(0)?.content\n if (content) returnManagedMenuItemsToSlot(this, content, this._level0ItemAnchors)\n }\n\n private _populateOverlay(level: number, submenuItem: HTMLElement) {\n const content = this._overlays.get(level)?.content\n if (content) moveMenuChildren(submenuItem, content)\n }\n\n private _depopulateOverlay(level: number, parentItem?: HTMLElement) {\n const content = this._overlays.get(level)?.content\n if (!content) return\n const targetLevel = level - 1\n const submenuItem = parentItem ?? this._getLevelItems(targetLevel)[this._activePath[targetLevel]]\n if (submenuItem) moveMenuChildren(content, submenuItem)\n }\n\n /*\n * 登记与「面板是否本帧新建」解耦,与 context-menu 的根层修复同构。\n *\n * 退场被打断时(`_closeRootAfterPresence` 在 `hideOverlayPresence` 返回 false 后提前\n * return)面板仍在 DOM 里,而关闭分支已经把句柄撤了。此时重开走的是 `_ensureOverlay`\n * 的两条复用分支,它们不会重跑 `_buildOverlay`——原先只有 `_buildOverlay` 会 claim/adopt,\n * 于是出现「面板可见 + `open === true` 却没有任何登记」:Escape 关不掉它(仲裁看不到它),\n * 守卫里 `contains` 恒 false,点面板内部反被当成外部点击关掉整张菜单。\n *\n * `open` 属性路径(`updated()` 里 scheduleFrame 到 `_ensureOverlay(0)`)与 `openMenu()`\n * 都落在复用分支上,因此**所有**根 claim 都必须走这里 —— 只在 `_ensureOverlay` 里补 claim\n * 是不够的:`openMenu()` 会先把句柄建好,使那次补 claim 变成 no-op,子层照样被漏掉。\n *\n * 新 claim 即新会话,旧会话连同其全部 adopt 子树一起作废,因此**仍在场的**子层必须\n * 在这里统一重挂。来源有两个:`_overlays` 里登记的层级,以及本轮已退场、被移出\n * `_overlays` 却仍在 DOM 里的收尾栈面板 —— 只看前者会漏掉后者(见下)。\n */\n private _claimRoot(panel: HTMLElement): OpenOverlayHandle {\n this._handle?.release()\n this._handle = this._overlay.claim(panel)\n for (const [level, sub] of this._overlays) {\n if (level === 0) continue\n sub.handle = this._handle.adopt(sub.overlay)\n }\n /*\n * 收尾栈里的子菜单面板同样「可见但已脱离快照」:`_closeSubmenuFrom` 只把它们移出\n * `_overlays`,层仍挂在旧句柄下、面板仍在 DOM 里退场。不重挂的话它们会被判成面板外,\n * 点它内部就关掉整张菜单 —— base 的 `composition.contains` 走登记树、且退场期间不注销,\n * 所以这条是相对 base 的行为回归,不是 parity。\n * 新句柄要写回容器:收尾结束时 adapter.dispose 释放的是 `container.handle`。\n */\n for (const container of this._closingSubmenus.closing()) {\n container.handle = this._handle.adopt(container.overlay)\n }\n return this._handle\n }\n\n /** 句柄缺失时补登记;面板还没建好时以宿主代填,与 `_openMenu` 的占位会话同一口径。 */\n private _ensureRootHandle(): OpenOverlayHandle {\n return this._handle ?? this._claimRoot(this._overlays.get(0)?.overlay ?? this)\n }\n\n private _ensureOverlay(level: number, isInstant = false, submenuItem?: HTMLElement): boolean {\n const closingOverlay = submenuItem ? this._closingSubmenus.take(submenuItem) : undefined\n if (closingOverlay) {\n // 从 closing 栈取回的面板:登记已在关闭时撤销,这里必须重新挂回根句柄子树。\n closingOverlay.handle = this._ensureRootHandle().adopt(closingOverlay.overlay)\n this._overlays.set(level, closingOverlay)\n closingOverlay.api.open()\n showOverlayPresence(closingOverlay.overlay, { isInstant })\n return true\n }\n const existing = this._overlays.get(level)\n if (existing) {\n this._ensureRootHandle()\n existing.api.open()\n showOverlayPresence(existing.overlay, { isInstant })\n return false\n }\n this._buildOverlay(level, isInstant)\n return false\n }\n\n private _buildOverlay(level: number, isInstant = false) {\n const { panel: overlay, content } = createMenuPortalOverlay('dropdown-overlay', this)\n overlay.setAttribute('role', 'menu')\n // 根层级面板带 id:trigger 的 aria-controls 指向它(Q9a)。\n if (level === 0) overlay.id = `wui-dropdown-menu-${++dropdownIdCounter}`\n overlay.dataset.level = String(level)\n overlay.addEventListener('click', this._onMenuClick)\n overlay.addEventListener('keydown', this._onKeydown)\n\n const anchor = level === 0 ? this._queryTriggerAnchor() : this._getSubmenuTriggerAnchor(level - 1)\n\n if (anchor) {\n const ctrl = defineOverlay().make({\n anchor,\n overlay,\n placement: level === 0 ? this.placement : 'right-start',\n offset: level === 0 ? this.offset : 0,\n // 菜单至少与 trigger/父项同宽,内容可更宽(floor 来自 --wui-overlay-min-width)\n minAnchorWidth: true,\n matchWidth: level === 0 ? this.matchWidth : false,\n strategy: 'fixed'\n })\n if (level === 0) {\n // 面板就绪:释放「以宿主代填」的占位会话,换成真实面板。\n this._claimRoot(overlay)\n this._overlays.set(level, { api: ctrl, overlay, content })\n this._populateLevel0()\n } else {\n /*\n * 子层进根句柄子树,默认不是独立候选 —— 「在树里但不是这一层」由此表达;\n * 原先按 level 找父面板的判断随之消失。用 `_ensureRootHandle()` 而不是 `_handle?.`:\n * 后者在句柄意外缺失时会静默跳过 adopt,留下一棵无人认领的子树。\n */\n const handle = this._ensureRootHandle().adopt(overlay)\n this._overlays.set(level, { api: ctrl, overlay, content, handle })\n }\n // submenu 父项在面板 content 内,其新增子项只有观察 content 子树才能看到\n this._menuContentObserver.observe(content, { childList: true, subtree: true })\n ctrl.open()\n showOverlayPresence(overlay, { isInstant })\n }\n }\n\n private _disposeOverlay(level: number) {\n const overlay = this._overlays.get(level)\n if (level === 0) {\n // 根句柄撤销会递归撤销全部已 adopt 的子层。\n this._handle?.release()\n this._handle = null\n }\n // 子层句柄随所属 MenuOverlay 释放;已被根层递归撤销时幂等无操作。\n overlay?.handle?.release()\n overlay?.overlay.removeEventListener('click', this._onMenuClick)\n overlay?.overlay.removeEventListener('keydown', this._onKeydown)\n overlay?.overlay.remove()\n overlay?.api.dispose()\n this._overlays.delete(level)\n this._rebindMenuContentObservers()\n }\n\n // jsdom 的 MutationObserver 不实现 unobserve;统一 disconnect 后重绑剩余目标。\n private _rebindMenuContentObservers() {\n this._menuContentObserver.disconnect()\n this._menuContentObserver.observe(this, { childList: true, subtree: true })\n for (const overlay of this._overlays.values()) {\n this._menuContentObserver.observe(overlay.content, { childList: true, subtree: true })\n }\n }\n\n private _disposeAll() {\n /*\n * 先无条件撤销根句柄。`_openMenu` 会先用宿主当 panel claim 一次占位会话(真实面板要等\n * 一帧才建好),若这段时间内宿主被卸载、`_buildOverlay` 没机会跑,`_overlays` 仍是空的\n * —— 只遍历 `_overlays` 就会漏掉它:该层永久留在模块注册表里强引用整个组件,且宿主\n * 重挂载后 `panel.isConnected` 又为真,占位层重新成为候选、吞掉 Escape。\n */\n this._handle?.release()\n this._handle = null\n for (const level of this._overlays.keys()) {\n this._disposeOverlay(level)\n }\n }\n\n private _syncScrollLock(isOpen = this.open) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n private _queryTriggerAnchor(): HTMLElement | null {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n const el = slot?.assignedElements()[0]\n return el instanceof HTMLElement ? el : null\n }\n\n /*\n * trigger 语义回写(Q9a):slot 进来的元素是真正的可聚焦交互元素,\n * aria-haspopup / aria-expanded / aria-controls 必须设在它身上;\n * 组件没有 trigger 包装 div,此前完全没有 ARIA 接线。\n */\n private _syncTriggerAria() {\n const trigger = this._queryTriggerAnchor()\n if (!trigger) return\n trigger.setAttribute('aria-haspopup', 'menu')\n trigger.setAttribute('aria-expanded', String(this.open))\n const rootOverlay = this._overlays.get(0)?.overlay\n if (rootOverlay?.id) trigger.setAttribute('aria-controls', rootOverlay.id)\n else trigger.removeAttribute('aria-controls')\n }\n\n private _getSubmenuTriggerAnchor(level: number): HTMLElement | null {\n const idx = this._activePath[level]\n if (idx === undefined) return null\n const items = this._getLevelItems(level)\n const item = items[idx]\n if (!item) return null\n return item.shadowRoot?.querySelector('.item-inner') ?? item\n }\n\n private _onTriggerClick = (event: MouseEvent) => {\n if (this.disabled) return\n if (this.open) {\n this._closeAll(true)\n } else {\n this._openMenu(event.detail === 0, true)\n }\n }\n\n private _onClickOutside = (e: MouseEvent) => {\n if (!this.isOpen || this._outsideClickGuard.isArmed()) return\n if (this._outsideClickGuard.isInside(e)) return\n this._closeAll(true)\n }\n\n private _onMenuClick = (e: MouseEvent) => {\n const item = getMenuItemFromEvent(e)\n if (!item || item.hasAttribute('disabled') || !item.hasAttribute('submenu')) return\n\n const level = this._getFocusedLevel(item)\n if (level === undefined) return\n const itemIndex = this._getLevelItems(level).indexOf(item)\n if (itemIndex >= 0) this._toggleSubmenu(level, itemIndex, e.detail === 0)\n }\n\n private _onKeydown = (e: KeyboardEvent) => {\n if (this.disabled || !this.open) return\n\n // overlay 上键盘事件 target 是菜单项而非组件自身,不能限定 e.target === this,\n // 否则鼠标打开、焦点在菜单项时 Escape 无法关闭\n handleMenuKeyboard(this._keyboardDelegate, e)\n }\n\n private _getFocusedLevel(item?: HTMLElement): number | undefined {\n if (!item) return undefined\n const overlay = [...this._overlays.entries()].find(([, value]) => value.overlay.contains(item))\n return overlay?.[0]\n }\n\n private _bindHoversAfterUpdate() {\n requestAnimationFrame(() => this._hoverBinder.bind())\n }\n\n override render() {\n return html`\n <div class=\"dropdown-trigger\" @click=${this._onTriggerClick}>\n <slot name=\"trigger\"></slot>\n </div>\n `\n }\n\n declare readonly $events: {\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-dropdown': WebUiDropdown\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA6CA,IAAM,IAAc,sBAEd,IAAmB,0BAErB,IAAoB,GAWX,IAAN,cAA4B,EAAW;;EAslBtB,aAnlB6B,KAAA,OAAA,IACI,KAAA,WAAA,IAC4B,KAAA,aAAA,IACK,KAAA,eAAA,IAExD,KAAA,aAAA,gBAUd,KAAA,UAAA,GAUuB,KAAA,cAAA,CAAC,GAEb,KAAA,4BAAA,IAAI,IAAyB,GAE9B,KAAA,WAAA,EAAkB,CAAC,CAAC,KAAK;GACnD,oBAAoB,KAAK,kBAAkB,kBAAkB;GAC7D,mBAAmB,KAAK;EAC1B,CAAC,GAC8B,KAAA,cAAA,EAAsB,CAAC,CAAC,KAAK,GAMhB,KAAA,UAAA,MAEN,KAAA,qBAAA,EACpC,OAEA,MAAQ,KAAK,SAAS,SAAS,CAAI,KAAK,EAC1C,GACoC,KAAA,mBAAA,EAAuC;GACzE,WAAU,MAAa,EAAU;GACjC,eAAe,GAAW,MAAe,EAAiB,EAAU,SAAS,CAAU;GACvF,UAAS,MAAa;IAGpB,AAFA,EAAU,QAAQ,QAAQ,GAC1B,EAAU,QAAQ,OAAO,GACzB,EAAU,IAAI,QAAQ;GACxB;EACF,CAAC,GACoD,KAAA,iBAAA;GACnD,oBAAoB,KAAK,YAAY;GACrC,gBAAe,MAAS,KAAK,eAAe,CAAK;GACjD,gBAAe,MAAS,KAAK,eAAe,CAAK,CAAC,CAAC,KAAK,YAAY;GACpE,gBAAe,MAAQ,EAAK,aAAa,SAAS;GAClD,cAAa,MAAQ;IACnB,IAAM,IAAQ,KAAK,iBAAiB,CAAI;IACxC,IAAI,MAAU,KAAA,GAAW;IACzB,IAAM,IAAY,KAAK,eAAe,CAAK,CAAC,CAAC,QAAQ,CAAI;IACzD,AAAI,KAAa,KAAG,KAAK,eAAe,GAAO,CAAS;GAC1D;GACA,YAAW,MAAS,KAAK,kBAAkB,IAAQ,CAAC;EACtD,GACgC,KAAA,eAAA,EAAsB,KAAK,gBAAgB,sBAAsB,GACtC,KAAA,oBAAA;GACzD,iBAAgB,MACd,EACE,GACA,CAAC,GAAG,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,iBAAc,CAAO,GACzD,sBACF;GACF,aAAY,MAAQ,KAAK,iBAAiB,CAAI;GAC9C,kBAAiB,MAAS,EAAyB,KAAK,UAAU,IAAI,CAAK,CAAC,EAAE,OAAO;GACrF,gBAAe,MAAQ,EAAK,aAAa,SAAS;GAClD,qBAAoB,MAAQ;IAC1B,IAAM,IAAQ,KAAK,iBAAiB,CAAI;IACxC,IAAI,MAAU,KAAA,GAAW;IACzB,IAAM,IAAY,KAAK,eAAe,CAAK,CAAC,CAAC,QAAQ,CAAI;IACzD,AAAI,KAAa,KAAG,KAAK,eAAe,GAAO,GAAW,EAAI;GAChE;GACA,gBAAe,OACb,KAAK,kBAAkB,CAAK,GACrB,KAAK,eAAe,IAAQ,CAAC,CAAC,CAAC,KAAK,YAAY,IAAQ;GAEjE,yBAAyB;IACvB,AAAI,KAAK,YAAY,SAAS,IAAG,KAAK,kBAAkB,KAAK,YAAY,MAAM,IAC1E,KAAK,UAAU,EAAI;GAC1B;EACF,GACmC,KAAA,kBAAA,IAAI,EAAqB,GAE7B,KAAA,uBAAA,IACF,KAAA,qBAAA,IAC0B,KAAA,qCAAA,IAAI,IAAI,GAKvB,KAAA,uBAAA,IAAI,uBAAuB;GACjE,AAAI,KAAK,QAAM,KAAK,kBAAkB;EACxC,CAAC,GAid0B,KAAA,mBAAA,MAAsB;GAC3C,KAAK,aACL,KAAK,OACP,KAAK,UAAU,EAAI,IAEnB,KAAK,UAAU,EAAM,WAAW,GAAG,EAAI;EAE3C,GAE2B,KAAA,mBAAA,MAAkB;GACvC,AAAC,KAAK,UAAU,MAAK,mBAAmB,QAAQ,MAChD,KAAK,mBAAmB,SAAS,CAAC,KACtC,KAAK,UAAU,EAAI;EACrB,GAEwB,KAAA,gBAAA,MAAkB;GACxC,IAAM,IAAO,EAAqB,CAAC;GACnC,IAAI,CAAC,KAAQ,EAAK,aAAa,UAAU,KAAK,CAAC,EAAK,aAAa,SAAS,GAAG;GAE7E,IAAM,IAAQ,KAAK,iBAAiB,CAAI;GACxC,IAAI,MAAU,KAAA,GAAW;GACzB,IAAM,IAAY,KAAK,eAAe,CAAK,CAAC,CAAC,QAAQ,CAAI;GACzD,AAAI,KAAa,KAAG,KAAK,eAAe,GAAO,GAAW,EAAE,WAAW,CAAC;EAC1E,GAEsB,KAAA,cAAA,MAAqB;GACrC,MAAK,YAAa,KAAK,QAI3B,EAAmB,KAAK,mBAAmB,CAAC;EAC9C;;;EA3lByB,KAAA,SAAA,EAAU,CAAK;;CASxC,IACI,YAAuB;EACzB,OAAO,KAAK;CACd;CACA,IAAI,UAAU,GAAW;EACvB,KAAK,aAAa,EAAiB,GAAG,GAAqB,cAAc;CAC3E;CAIA,IACI,SAAiB;EACnB,OAAO,KAAK;CACd;CACA,IAAI,OAAO,GAAW;EACpB,KAAK,UAAU,EAAgB,GAAG,GAAG,KAAK,CAAC;CAC7C;CAoFA,IAAI,SAAkB;EACpB,OAAO,KAAK,QAAQ,KAAK,YAAY,SAAS;CAChD;CAEA,oBAA6B;EAa3B,AAZA,MAAM,kBAAkB,GACxB,KAAK,SAAS,OAAO,GACrB,KAAK,iBAAiB,WAAW,KAAK,UAAU,GAChD,SAAS,iBAAiB,SAAS,KAAK,eAAe,GACvD,KAAK,qBAAqB,QAAQ,MAAM;GAAE,WAAW;GAAM,SAAS;EAAK,CAAC,GAQtE,KAAK,QAAQ,KAAK,cAAY,KAAK,iBAAiB,KAAK,iBAAiB,CAAC;CACjF;CAEA,eAAwB;EAGtB,KAAK,YACD,cAA+B,wBAAsB,CAAC,EACtD,iBAAiB,oBAAoB,KAAK,cAAc,CAAC;CAC/D;CAEA,uBAAgC;EAU9B,AATA,MAAM,qBAAqB,GAC3B,KAAK,qBAAqB,WAAW,GACrC,KAAK,oBAAoB,WAAW,KAAK,UAAU,GACnD,SAAS,oBAAoB,SAAS,KAAK,eAAe,GAC1D,KAAK,mBAAmB,QAAQ,GAChC,KAAK,aAAa,QAAQ,GAE1B,KAAK,SAAS,QAAQ,GACtB,KAAK,YAAY,QAAQ,GACzB,KAAK,mBAAmB;CAC1B;CAEA,QAA2B,GAA+B;EA8BxD,AA5BA,KAAK,iBAAiB,GAElB,EAAQ,IAAI,MAAM,MAChB,KAAK,OACP,KAAK,iBAAiB,EAAI,KAE1B,KAAK,SAAS,WAAW,GACzB,KAAK,gBAAgB,GAKrB,KAAK,SAAS,QAAQ,GACtB,KAAK,UAAU,MACf,KAAU,wBAAwB,IAEhC,KAAK,gBAAgB,QAAQ,KAC/B,EAAwB,MAAM,KAAK,IAAI,IAGvC,EAAQ,IAAI,cAAc,KAAG,KAAK,gBAAgB,IAClD,EAAQ,IAAI,WAAW,KAAK,EAAQ,IAAI,QAAQ,KAAK,EAAQ,IAAI,YAAY,MAC/E,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE,IAAI,OAAO;GAChC,WAAW,KAAK;GAChB,QAAQ,KAAK;GACb,YAAY,KAAK;EACnB,CAAC,GAEH,KAAK,aAAa,KAAK;CACzB;CAMA,mBAAoC;EAClC,IAAM,IAAS,SAAS;EACxB,OAAO,MAAW,QAAQ,MAAW,SAAS,QAAQ,MAAW,SAAS;CAC5E;CAMA,iBAAyB,GAAoB;EAa3C,AAZA,KAAK,wBAAwB,SAAS,yBAAyB,cAAc,SAAS,gBAAgB,KAAA,GACtG,KAAK,mBAAmB,IAAI,GAC5B,KAAK,gBAAgB,GACrB,KAAK,wBAAwB,GAC7B,KAAK,SAAS,WAAW,GACzB,KAAK,SAAS,oBAAoB;GAKhC,AAJA,KAAK,eAAe,GAAG,KAAK,oBAAoB,GAChD,KAAK,uBAAuB,IACxB,KAAW,EAAc,EAAyB,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,GAExF,KAAK,iBAAiB;EACxB,CAAC,GACD,KAAK,uBAAuB;CAC9B;CAGA,WAAW;EACT,KAAK,UAAU,IAAM,EAAK;CAC5B;CAEA,UAAkB,GAAoB,GAAmB;EACnD,KAAK,YAAY,KAAK,SAC1B,KAAK,mBAAmB,IAAI,GAC5B,KAAK,uBAAuB,GACxB,KAAU,KAAK,gBAAgB,KAAK,GAGxC,KAAK,WAAW,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE,WAAW,IAAI,GACtD,KAAK,OAAO;CACd;CAGA,WAAW;EACT,KAAK,UAAU,EAAK;CACtB;CAEA,UAAkB,GAAmB;EAC9B,AAEL,KAAK,UADD,KAAU,KAAK,gBAAgB,KAAK,GAC5B;CACd;CAGA,oBAA4B;EAG1B,AAFA,KAAK,kBAAkB,GAAG,EAAI,GAC9B,KAAK,iBAAiB,WAAW,GACjC,KAAK,iBAAiB;CACxB;CAEA,qBAA6B;EAG3B,AAFA,KAAK,kBAAkB,GACvB,KAAK,mBAAmB,GACxB,KAAK,YAAY;CACnB;CAEA,MAAc,0BAA0B;EACtC,IAAM,IAAU,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE;EACnC,OAAa,MAAM,EAAoB,CAAO,MAC9C,MAAK,QAAS,KAAK,gBAEvB,KAAK,mBAAmB,GACxB,KAAK,qBAAqB,MAAM,GAChC,KAAK,sBAAsB,KAAA;CAC7B;CAEA,0BAAkC;EAChC,EAAuB,MAAM,GAAG,EAAY,QAAQ;CACtD;CAEA,eAAuB,GAAe,GAAmB,IAAY,IAAO;EAC1E,IAAI,KAAK,YAAY,OAAW,GAC9B;EAMF,AAHA,KAAK,kBAAkB,IAAQ,CAAC,GAEhC,KAAK,cAAc,CAAC,GAAG,KAAK,YAAY,MAAM,GAAG,CAAK,GAAG,CAAS,GAClE,KAAK,iBAAiB;EAEtB,IAAM,IAAO,KAAK,eAAe,CAAK,CAAC,CAAC;EASxC,AARI,GAAM,aAAa,SAAS,KAC9B,KAAK,SAAS,oBAAoB;GAC5B,AAAC,KAAK,QAAQ,KAAK,YAAY,OAAW,MAC7B,KAAK,eAAe,IAAQ,GAAG,GAAW,CACtD,KAAU,KAAK,iBAAiB,IAAQ,GAAG,CAAI;EACtD,CAAC,GAGH,KAAK,uBAAuB;CAC9B;CAEA,kBAA0B,GAAe,IAAY,IAAO;EAC1D,KAAK,IAAI,IAAK,KAAK,YAAY,QAAQ,KAAM,GAAO,KAAM;GACxD,IAAM,IAAU,KAAK,UAAU,IAAI,CAAE,GAC/B,IAAa,KAAK,eAAe,IAAK,CAAC,CAAC,CAAC,KAAK,YAAY,IAAK;GAChE,MAED,CAAC,KAAc,KACjB,KAAK,mBAAmB,GAAI,CAAU,GACtC,KAAK,gBAAgB,CAAE,MAEvB,KAAK,UAAU,OAAO,CAAE,GACxB,KAAK,iBAAiB,WAAW,GAAY,CAAO;EAExD;EACA,IAAM,IAAiB,KAAK,YAAY,MAAM,GAAG,IAAQ,CAAC;EAI1D,AAHI,EAAe,WAAW,KAAK,YAAY,WAC7C,KAAK,cAAc,IAErB,KAAK,iBAAiB;CACxB;CAEA,mBAA2B;EAOzB,AANA,KAAK,iBAAiB,sBAAsB,CAAC,CAAC,SAAQ,MAAQ;GAC5D,EAAK,gBAAgB,QAAQ;EAC/B,CAAC,GACD,KAAK,UAAU,SAAS,GAAG,MAAU;GACnC,KAAK,eAAe,CAAK,CAAC,CAAC,SAAQ,MAAQ,EAAK,gBAAgB,QAAQ,CAAC;EAC3E,CAAC,GACD,KAAK,YAAY,SAAS,GAAW,MAAU;GAC7C,IAAM,IAAO,KAAK,eAAe,CAAK,CAAC,CAAC;GACxC,AAAI,GAAM,QAAQ,sBAAsB,KACtC,EAAK,aAAa,UAAU,EAAE;EAElC,CAAC;CACH;CAEA,eAAuB,GAA8B;EACnD,IAAM,IAAU,KAAK,UAAU,IAAI,CAAK,CAAC,EAAE;EAC3C,OAAO,IAAU,EAAgB,CAAO,IAAI,CAAC;CAC/C;CAEA,kBAA0B;EACxB,IAAM,IAAU,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE;EAClC,MAGL,EAA0B,MAAM,GAAS,KAAK,oBAAoB,CAAgB,GAClF,EAAsB,MAAM,GAAS,KAAK,oBAAoB,CAAgB;CAChF;CAEA,oBAA4B;EACtB,KAAK,uBACT,KAAK,qBAAqB,IAC1B,4BAA4B;GAC1B,KAAK,qBAAqB,IACrB,KAAK,QACV,KAAK,qBAAqB;EAC5B,CAAC;CACH;CAKA,uBAA+B;EAC7B,IAAM,IAAgB,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE;EAG7C,IADA,KAAK,wBAAwB,GACzB,GAAe;GAIjB,AAHA,EAA0B,MAAM,GAAe,KAAK,oBAAoB,CAAgB,GAGxF,EAAuB,GAAe,GAAG,EAAY,QAAQ;GAE7D,IAAM,IAAmB,EAAwB,GAAe,CAAgB;GAEhF,AADA,EAAsB,MAAM,GAAe,KAAK,oBAAoB,CAAgB,GACpF,EAAwB,GAAe,CAAgB;EACzD;EACA,KAAK,IAAM,CAAC,GAAO,MAAY,KAAK,WAAW;GAC7C,IAAI,MAAU,GAAG;GACjB,IAAM,IAAa,KAAK,eAAe,IAAQ,CAAC,CAAC,CAAC,KAAK,YAAY,IAAQ;GAE3E,AADI,KAAY,EAAiB,GAAY,EAAQ,OAAO,GAC5D,EAAuB,EAAQ,SAAS,GAAG,EAAY,QAAQ;EACjE;EACA,KAAK,aAAa,KAAK;CACzB;CAEA,qBAA6B;EAC3B,IAAM,IAAU,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE;EACvC,AAAI,KAAS,EAA6B,MAAM,GAAS,KAAK,kBAAkB;CAClF;CAEA,iBAAyB,GAAe,GAA0B;EAChE,IAAM,IAAU,KAAK,UAAU,IAAI,CAAK,CAAC,EAAE;EAC3C,AAAI,KAAS,EAAiB,GAAa,CAAO;CACpD;CAEA,mBAA2B,GAAe,GAA0B;EAClE,IAAM,IAAU,KAAK,UAAU,IAAI,CAAK,CAAC,EAAE;EAC3C,IAAI,CAAC,GAAS;EACd,IAAM,IAAc,IAAQ,GACtB,IAAc,KAAc,KAAK,eAAe,CAAW,CAAC,CAAC,KAAK,YAAY;EACpF,AAAI,KAAa,EAAiB,GAAS,CAAW;CACxD;CAmBA,WAAmB,GAAuC;EAExD,AADA,KAAK,SAAS,QAAQ,GACtB,KAAK,UAAU,KAAK,SAAS,MAAM,CAAK;EACxC,KAAK,IAAM,CAAC,GAAO,MAAQ,KAAK,WAC1B,MAAU,MACd,EAAI,SAAS,KAAK,QAAQ,MAAM,EAAI,OAAO;EAS7C,KAAK,IAAM,KAAa,KAAK,iBAAiB,QAAQ,GACpD,EAAU,SAAS,KAAK,QAAQ,MAAM,EAAU,OAAO;EAEzD,OAAO,KAAK;CACd;CAGA,oBAA+C;EAC7C,OAAO,KAAK,WAAW,KAAK,WAAW,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE,WAAW,IAAI;CAC/E;CAEA,eAAuB,GAAe,IAAY,IAAO,GAAoC;EAC3F,IAAM,IAAiB,IAAc,KAAK,iBAAiB,KAAK,CAAW,IAAI,KAAA;EAC/E,IAAI,GAMF,OAJA,EAAe,SAAS,KAAK,kBAAkB,CAAC,CAAC,MAAM,EAAe,OAAO,GAC7E,KAAK,UAAU,IAAI,GAAO,CAAc,GACxC,EAAe,IAAI,KAAK,GACxB,EAAoB,EAAe,SAAS,EAAE,aAAU,CAAC,GAClD;EAET,IAAM,IAAW,KAAK,UAAU,IAAI,CAAK;EAQzC,OAPI,KACF,KAAK,kBAAkB,GACvB,EAAS,IAAI,KAAK,GAClB,EAAoB,EAAS,SAAS,EAAE,aAAU,CAAC,GAC5C,OAET,KAAK,cAAc,GAAO,CAAS,GAC5B;CACT;CAEA,cAAsB,GAAe,IAAY,IAAO;EACtD,IAAM,EAAE,OAAO,GAAS,eAAY,EAAwB,oBAAoB,IAAI;EAMpF,AALA,EAAQ,aAAa,QAAQ,MAAM,GAE/B,MAAU,MAAG,EAAQ,KAAK,qBAAqB,EAAE,MACrD,EAAQ,QAAQ,QAAQ,OAAO,CAAK,GACpC,EAAQ,iBAAiB,SAAS,KAAK,YAAY,GACnD,EAAQ,iBAAiB,WAAW,KAAK,UAAU;EAEnD,IAAM,IAAS,MAAU,IAAI,KAAK,oBAAoB,IAAI,KAAK,yBAAyB,IAAQ,CAAC;EAEjG,IAAI,GAAQ;GACV,IAAM,IAAO,EAAc,CAAC,CAAC,KAAK;IAChC;IACA;IACA,WAAW,MAAU,IAAI,KAAK,YAAY;IAC1C,QAAQ,MAAU,IAAI,KAAK,SAAS;IAEpC,gBAAgB;IAChB,YAAY,MAAU,KAAI,KAAK;IAC/B,UAAU;GACZ,CAAC;GACD,IAAI,MAAU,GAIZ,AAFA,KAAK,WAAW,CAAO,GACvB,KAAK,UAAU,IAAI,GAAO;IAAE,KAAK;IAAM;IAAS;GAAQ,CAAC,GACzD,KAAK,gBAAgB;QAChB;IAML,IAAM,IAAS,KAAK,kBAAkB,CAAC,CAAC,MAAM,CAAO;IACrD,KAAK,UAAU,IAAI,GAAO;KAAE,KAAK;KAAM;KAAS;KAAS;IAAO,CAAC;GACnE;GAIA,AAFA,KAAK,qBAAqB,QAAQ,GAAS;IAAE,WAAW;IAAM,SAAS;GAAK,CAAC,GAC7E,EAAK,KAAK,GACV,EAAoB,GAAS,EAAE,aAAU,CAAC;EAC5C;CACF;CAEA,gBAAwB,GAAe;EACrC,IAAM,IAAU,KAAK,UAAU,IAAI,CAAK;EAaxC,AAZI,MAAU,MAEZ,KAAK,SAAS,QAAQ,GACtB,KAAK,UAAU,OAGjB,GAAS,QAAQ,QAAQ,GACzB,GAAS,QAAQ,oBAAoB,SAAS,KAAK,YAAY,GAC/D,GAAS,QAAQ,oBAAoB,WAAW,KAAK,UAAU,GAC/D,GAAS,QAAQ,OAAO,GACxB,GAAS,IAAI,QAAQ,GACrB,KAAK,UAAU,OAAO,CAAK,GAC3B,KAAK,4BAA4B;CACnC;CAGA,8BAAsC;EAEpC,AADA,KAAK,qBAAqB,WAAW,GACrC,KAAK,qBAAqB,QAAQ,MAAM;GAAE,WAAW;GAAM,SAAS;EAAK,CAAC;EAC1E,KAAK,IAAM,KAAW,KAAK,UAAU,OAAO,GAC1C,KAAK,qBAAqB,QAAQ,EAAQ,SAAS;GAAE,WAAW;GAAM,SAAS;EAAK,CAAC;CAEzF;CAEA,cAAsB;EAQpB,AADA,KAAK,SAAS,QAAQ,GACtB,KAAK,UAAU;EACf,KAAK,IAAM,KAAS,KAAK,UAAU,KAAK,GACtC,KAAK,gBAAgB,CAAK;CAE9B;CAEA,gBAAwB,IAAS,KAAK,MAAM;EAC1C,KAAK,YAAY,KAAK,KAAU,CAAC,KAAK,YAAY;CACpD;CAEA,sBAAkD;EAEhD,IAAM,KADO,KAAK,YAAY,cAA+B,wBAAsB,EAAA,EAClE,iBAAiB,CAAC,CAAC;EACpC,OAAO,aAAc,cAAc,IAAK;CAC1C;CAOA,mBAA2B;EACzB,IAAM,IAAU,KAAK,oBAAoB;EACzC,IAAI,CAAC,GAAS;EAEd,AADA,EAAQ,aAAa,iBAAiB,MAAM,GAC5C,EAAQ,aAAa,iBAAiB,OAAO,KAAK,IAAI,CAAC;EACvD,IAAM,IAAc,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE;EAC3C,AAAI,GAAa,KAAI,EAAQ,aAAa,iBAAiB,EAAY,EAAE,IACpE,EAAQ,gBAAgB,eAAe;CAC9C;CAEA,yBAAiC,GAAmC;EAClE,IAAM,IAAM,KAAK,YAAY;EAC7B,IAAI,MAAQ,KAAA,GAAW,OAAO;EAE9B,IAAM,IADQ,KAAK,eAAe,CACrB,CAAA,CAAM;EAEnB,OADK,IACE,EAAK,YAAY,cAAc,aAAa,KAAK,IADtC;CAEpB;CAmCA,iBAAyB,GAAwC;EAC1D,OAEL,OADgB,CAAC,GAAG,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,MAAM,GAAG,OAAW,EAAM,QAAQ,SAAS,CAAI,CACtF,CAAA,GAAU;CACnB;CAEA,yBAAiC;EAC/B,4BAA4B,KAAK,aAAa,KAAK,CAAC;CACtD;CAEA,SAAkB;EAChB,OAAO,CAAI;6CAC8B,KAAK,gBAAgB;;;;CAIhE;AAKF;AAhnBG,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;CAAM,WAAW;AAAc,CAAC,CAAA,GAAA,EAAA,WAAA,cAAA,KAAA,CAAA,GACnE,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;CAAM,WAAW;AAAiB,CAAC,CAAA,GAAA,EAAA,WAAA,gBAAA,KAAA,CAAA,GAItE,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,aAAA,IAAA,GAUxC,EAAA,CAAA,EAAS,EAAE,MAAM,OAAO,CAAC,CAAA,GAAA,EAAA,WAAA,UAAA,IAAA,GAQzB,EAAA,CAAA,EAAM,CAAA,GAAA,EAAA,WAAA,eAAA,KAAA,CAAA,GA7BR,IAAA,EAAA,CAAA,EAAc,iBAAiB,CAAA,GAAA,CAAA"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/dropdown/index.ts"],"sourcesContent":["import type { Placement } from '@floating-ui/dom'\nimport { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { UserChangeController } from '@/shared/events/user-change'\nimport {\n createClosingSubmenuStack,\n createMenuHoverBinder,\n createMenuOutsideClickGuard,\n getEnabledMenuLevelItems,\n getFocusedMenuItemFromPanels,\n handleMenuKeyboard,\n type MenuHoverDelegate,\n type MenuKeyboardDelegate\n} from '@/shared/menu-behavior'\nimport { createMenuPortalOverlay } from '@/shared/menu-portal/menu-portal'\nimport {\n captureFrameworkAnchors,\n focusMenuItem,\n getMenuChildren,\n getMenuItemFromEvent,\n hideNestedMenuChildren,\n moveMenuChildren,\n orderManagedMenuItems,\n reconcileManagedMenuItems,\n restoreFrameworkAnchors,\n returnManagedMenuItemsToSlot,\n type MenuItemAnchors\n} from '@/shared/menu-portal/menu-tree'\nimport { normalizeLiteral, normalizeNumber } from '@/shared/normalize'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { defineOpenOverlay, type OpenOverlayHandle } from '@/shared/overlay/open-overlay'\nimport { defineOverlay, type OverlayApi } from '@/shared/overlay/overlay'\nimport { FLOATING_PLACEMENTS } from '@/shared/overlay/placement-props'\nimport { hideOverlayPresence, showOverlayPresence } from '@/shared/overlay/presence'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\n\nimport style from './style.css?inline'\n\nexport type { Placement }\n\nconst SLOT_PREFIX = 'web-ui-menu-level-'\n// 一级菜单项的模板位 marker(机制与 context-menu 共用,见 menu-tree 的托管项锚说明)。\nconst MENU_ITEM_MARKER = 'wui-dropdown-menu-item'\n\nlet dropdownIdCounter = 0\n\ninterface MenuOverlay {\n api: OverlayApi\n overlay: HTMLElement\n content: HTMLElement\n /** 该层为子菜单时的会话句柄;根层句柄由组件单独持有(见 `_handle`)。 */\n handle?: OpenOverlayHandle\n}\n\n@customElement('web-ui-dropdown')\nexport class WebUiDropdown extends LitElement {\n static override styles = unsafeCSS(style)\n\n @property({ type: Boolean, reflect: true }) open = false\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true, attribute: 'match-width' }) matchWidth = false\n @property({ type: Boolean, reflect: true, attribute: 'no-scroll-lock' }) noScrollLock = false\n\n private _placement: Placement = 'bottom-start'\n\n @property({ type: String, reflect: true })\n get placement(): Placement {\n return this._placement\n }\n set placement(v: string) {\n this._placement = normalizeLiteral(v, FLOATING_PLACEMENTS, 'bottom-start')\n }\n\n private _offset = 4\n\n @property({ type: Number })\n get offset(): number {\n return this._offset\n }\n set offset(v: number) {\n this._offset = normalizeNumber(v, 0, 100, 4)\n }\n\n @state() private _activePath: number[] = []\n\n private readonly _overlays = new Map<number, MenuOverlay>()\n /** 实例作用域的帧事务入口(合并了原 lifecycle)。 */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => this._keyboardDelegate.closeDeepestOrAll(),\n isConnected: () => this.isConnected\n })\n private readonly _scrollLock = defineScrollLockLease().make()\n /**\n * 当前 Escape 候选的会话句柄(level 0)。面板要等一帧才建好,而 Escape 可能在这之前\n * 到达:先以宿主为 panel claim 一次,面板就绪后 release 旧会话、再以真实面板重新 claim。\n * 子菜单的句柄存在各自 `MenuOverlay.handle` 上(经 adopt 进本句柄子树)。\n */\n private _handle: OpenOverlayHandle | null = null\n // 行为层(hover / outside-click / 键盘 / submenu 收尾)由 shared/menu-behavior 驱动\n private readonly _outsideClickGuard = createMenuOutsideClickGuard(\n this,\n // 根句柄的 contains 递归覆盖全部已 adopt 的子层,一次查询替代逐层判定。\n node => this._handle?.contains(node) ?? false\n )\n private readonly _closingSubmenus = createClosingSubmenuStack<MenuOverlay>({\n getPanel: container => container.overlay,\n restoreItems: (container, parentItem) => moveMenuChildren(container.content, parentItem),\n dispose: container => {\n container.handle?.release()\n container.overlay.remove()\n container.api.dispose()\n }\n })\n private readonly _hoverDelegate: MenuHoverDelegate = {\n getOpenDepth: () => this._activePath.length,\n getLevelItems: level => this._getLevelItems(level),\n getActiveItem: level => this._getLevelItems(level)[this._activePath[level]],\n isSubmenuItem: item => item.hasAttribute('submenu'),\n openSubmenu: item => {\n const level = this._getFocusedLevel(item)\n if (level === undefined) return\n const itemIndex = this._getLevelItems(level).indexOf(item)\n if (itemIndex >= 0) this._toggleSubmenu(level, itemIndex)\n },\n closeFrom: level => this._closeSubmenuFrom(level + 1)\n }\n private readonly _hoverBinder = createMenuHoverBinder(this._hoverDelegate, 'web-ui-dropdown-item')\n private readonly _keyboardDelegate: MenuKeyboardDelegate = {\n getFocusedItem: event =>\n getFocusedMenuItemFromPanels(\n event,\n [...this._overlays.values()].map(({ overlay }) => overlay),\n 'web-ui-dropdown-item'\n ),\n getLevelOf: item => this._getFocusedLevel(item),\n getEnabledItems: level => getEnabledMenuLevelItems(this._overlays.get(level)?.content),\n isSubmenuItem: item => item.hasAttribute('submenu'),\n openSubmenuInstant: item => {\n const level = this._getFocusedLevel(item)\n if (level === undefined) return\n const itemIndex = this._getLevelItems(level).indexOf(item)\n if (itemIndex >= 0) this._toggleSubmenu(level, itemIndex, true)\n },\n closeToParent: level => {\n this._closeSubmenuFrom(level)\n return this._getLevelItems(level - 1)[this._activePath[level - 1]]\n },\n closeDeepestOrAll: () => {\n if (this._activePath.length > 0) this._closeSubmenuFrom(this._activePath.length)\n else this._closeAll(true)\n }\n }\n private readonly _userOpenChange = new UserChangeController()\n private _restoreFocusTarget?: HTMLElement\n private _shouldOpenInstantly = true\n /**\n * 本次打开是否抑制首项的 `:focus-visible` accent 视觉(口径与 context-menu 一致)。\n *\n * 默认 `true`(抑制),只有键盘激活触发的打开才置 `false`:首项的焦点是组件程序化\n * 落的,而各引擎对「程序化 focus 是否算 focus-visible」判定不同 —— Chromium 走启发式\n * 通常不匹配,WKWebView 则会匹配(用户在 Interweave 桌面 app 实测到蓝底白字),\n * 于是同一段代码只在 WebKit 侧暴露出缺陷。取默认抑制而非默认保留,是因为冷启动的\n * `open` 属性路径拿不到任何手势模态信息,抑制是唯一确定安全的一侧;误伤键盘用户的\n * 代价有界:`handleMenuKeyboard` 首次方向键即以无参 `focusMenuItem` 重新落焦,\n * `menu-tree` 的 `toggleAttribute` 无条件清掉本属性,accent 当场回来。\n */\n private _suppressInitialFocusVisible = true\n private _menuSyncScheduled = false\n private readonly _level0ItemAnchors: MenuItemAnchors = new Map()\n // 打开期实时渲染:框架(Vue/React 条件渲染)可能在菜单打开期间持续插入菜单项。\n // 观察宿主子树(新增项可能被框架 wrapper 包裹)与各层级面板 content 的子树\n // (打开中的 submenu 父项已被迁入 content,其新增子项只在这里可见),下一帧统一\n // 把新子树迁入对应层级面板;面板尺寸变化由 defineOverlay 的 autoUpdate 跟进重定位。\n private readonly _menuContentObserver = new MutationObserver(() => {\n if (this.open) this._scheduleMenuSync()\n })\n\n get isOpen(): boolean {\n return this.open || this._activePath.length > 0\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._overlay.resume()\n this.addEventListener('keydown', this._onKeydown)\n document.addEventListener('click', this._onClickOutside)\n this._menuContentObserver.observe(this, { childList: true, subtree: true })\n /*\n * 重挂载对账:`disconnectedCallback` 会拆掉面板、把菜单项迁回宿主,但 `open` 是公开\n * prop,不因卸载而改写。Lit 在 detach 期间不记 `changedProperties`,重连后 `updated()`\n * 不再命中 `changed.has('open')`,面板于是永久缺席:宿主仍反射 `open` 与\n * `aria-expanded`,Escape 和 outside click 却无层可关。首连时 `open` 在\n * `changedProperties` 里、由 `updated()` 负责,`hasUpdated` 就是用来只认重连的。\n */\n if (this.open && this.hasUpdated) this._syncOpenOverlay(this._isFocusOrphaned())\n }\n\n override firstUpdated() {\n // trigger slot 内容增删不触发宿主响应式更新:显式请求一轮渲染,\n // 由 updated() 的 ARIA 回写覆盖晚到的 trigger 元素。\n this.shadowRoot\n ?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n ?.addEventListener('slotchange', () => this.requestUpdate())\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._menuContentObserver.disconnect()\n this.removeEventListener('keydown', this._onKeydown)\n document.removeEventListener('click', this._onClickOutside)\n this._outsideClickGuard.dispose()\n this._hoverBinder.dispose()\n // 停止帧调度;登记由 _cleanupClosedMenu → _disposeAll 撤销。\n this._overlay.suspend()\n this._scrollLock.release()\n this._cleanupClosedMenu()\n }\n\n protected override updated(changed: Map<string, unknown>) {\n // ARIA 回写不依赖 open 分支,任何渲染后都保持与宿主状态同步。\n this._syncTriggerAria()\n\n if (changed.has('open')) {\n if (this.open) {\n this._syncOpenOverlay(true)\n } else {\n this._overlay.invalidate()\n this._syncScrollLock()\n /*\n * 复位打开模态:`open` 真正翻回 false 才是「上一次打开会话结束」的唯一信号,\n * 下一会话若不经 `_openMenu()`(宿主直接翻 `open` 属性)就从默认口径重新起算,\n * 不会把上一次键盘打开留下的 `false` 带进这次鼠标驱动的声明式打开。\n * 刻意不复位在 `disconnectedCallback`/`_cleanupClosedMenu`:重挂载对账(:188)\n * 期间 `open` 始终为 true,那次重取焦点属于同一会话,应保留原模态。\n */\n this._suppressInitialFocusVisible = true\n /*\n * release ⟺ 关闭:新模块里「已登记」就等于「参与仲裁」,不再由 host.isOpen()\n * 兜底过滤,所以必须在这里撤销,不能等到退场动画结束。\n */\n this._handle?.release()\n this._handle = null\n void this._closeRootAfterPresence()\n }\n if (this._userOpenChange.consume()) {\n dispatchOpenChangeEvent(this, this.open)\n }\n }\n if (changed.has('noScrollLock')) this._syncScrollLock()\n if (changed.has('placement') || changed.has('offset') || changed.has('matchWidth')) {\n this._overlays.get(0)?.api.update({\n placement: this.placement,\n offset: this.offset,\n matchWidth: this.matchWidth\n })\n }\n this._hoverBinder.bind()\n }\n\n /**\n * 焦点是否已无归属:被 detach 打掉的焦点会落回 `body`,用户主动点到别处时不会。\n * 重挂载对账只在前者才把焦点夺回菜单,否则等于从消费者手里抢焦点。\n */\n private _isFocusOrphaned(): boolean {\n const active = document.activeElement\n return active === null || active === document.body || active === document.documentElement\n }\n\n /**\n * 打开态的唯一对账入口:面板、滚动锁、outside click、hover 与菜单焦点都从这里收敛,\n * `updated()` 的打开分支与重连对账共用它,两条路径必须落到同一个状态。\n */\n private _syncOpenOverlay(takeFocus: boolean) {\n this._restoreFocusTarget ??= document.activeElement instanceof HTMLElement ? document.activeElement : undefined\n this._outsideClickGuard.arm()\n this._syncScrollLock()\n this._hideAllSubmenuChildren()\n this._overlay.invalidate()\n this._overlay.scheduleFrame(() => {\n this._ensureOverlay(0, this._shouldOpenInstantly)\n this._shouldOpenInstantly = true\n if (takeFocus) {\n focusMenuItem(getEnabledMenuLevelItems(this._overlays.get(0)?.content)[0], {\n suppressFocusVisible: this._suppressInitialFocusVisible\n })\n }\n // 面板 id 在 overlay 构建后才有:回写 aria-controls 指向。\n this._syncTriggerAria()\n })\n this._bindHoversAfterUpdate()\n }\n\n /*\n * 命令式打开,不派发 open-change。命令式入口没有手势模态信息,按与 context-menu 的\n * `openAt()` 同一口径抑制首项 accent。\n */\n openMenu() {\n this._openMenu(true, false, true)\n }\n\n private _openMenu(isInstant: boolean, fromUser: boolean, suppressFocusVisible: boolean) {\n if (this.disabled || this.open) return\n this._outsideClickGuard.arm()\n this._shouldOpenInstantly = isInstant\n this._suppressInitialFocusVisible = suppressFocusVisible\n if (fromUser) this._userOpenChange.mark()\n // 面板要等一帧才建好,而 Escape 可能在这之前到达:先用宿主当 panel claim 一次;\n // 面板就绪后 _buildOverlay 会 release 这个占位会话,再以真实面板重新 claim。\n this._claimRoot(this._overlays.get(0)?.overlay ?? this)\n this.open = true\n }\n\n // 命令式关闭全部层级,不派发 open-change。\n closeAll() {\n this._closeAll(false)\n }\n\n private _closeAll(fromUser: boolean) {\n if (!this.open) return\n if (fromUser) this._userOpenChange.mark()\n this.open = false\n }\n\n private _closeAllSubmenus() {\n this._closeSubmenuFrom(1, true)\n this._closingSubmenus.restoreAll()\n this._syncActiveAttrs()\n }\n\n private _cleanupClosedMenu() {\n this._closeAllSubmenus()\n this._returnLevel0Items()\n this._disposeAll()\n }\n\n private async _closeRootAfterPresence() {\n const overlay = this._overlays.get(0)?.overlay\n if (overlay && !(await hideOverlayPresence(overlay))) return\n if (this.open || !this.isConnected) return\n\n this._cleanupClosedMenu()\n this._restoreFocusTarget?.focus()\n this._restoreFocusTarget = undefined\n }\n\n private _hideAllSubmenuChildren() {\n hideNestedMenuChildren(this, `${SLOT_PREFIX}-hidden`)\n }\n\n private _toggleSubmenu(level: number, itemIndex: number, isInstant = false) {\n if (this._activePath[level] === itemIndex) {\n return\n }\n\n this._closeSubmenuFrom(level + 1)\n\n this._activePath = [...this._activePath.slice(0, level), itemIndex]\n this._syncActiveAttrs()\n\n const item = this._getLevelItems(level)[itemIndex]\n if (item?.hasAttribute('submenu')) {\n this._overlay.scheduleFrame(() => {\n if (!this.open || this._activePath[level] !== itemIndex) return\n const restored = this._ensureOverlay(level + 1, isInstant, item)\n if (!restored) this._populateOverlay(level + 1, item)\n })\n }\n\n this._bindHoversAfterUpdate()\n }\n\n private _closeSubmenuFrom(level: number, isInstant = false) {\n for (let lv = this._activePath.length; lv >= level; lv--) {\n const overlay = this._overlays.get(lv)\n const parentItem = this._getLevelItems(lv - 1)[this._activePath[lv - 1]]\n if (!overlay) continue\n\n if (!parentItem || isInstant) {\n this._depopulateOverlay(lv, parentItem)\n this._disposeOverlay(lv)\n } else {\n this._overlays.delete(lv)\n this._closingSubmenus.closeAsync(parentItem, overlay)\n }\n }\n const nextActivePath = this._activePath.slice(0, level - 1)\n if (nextActivePath.length !== this._activePath.length) {\n this._activePath = nextActivePath\n }\n this._syncActiveAttrs()\n }\n\n private _syncActiveAttrs() {\n this.querySelectorAll('web-ui-dropdown-item').forEach(item => {\n item.removeAttribute('active')\n })\n this._overlays.forEach((_, level) => {\n this._getLevelItems(level).forEach(item => item.removeAttribute('active'))\n })\n this._activePath.forEach((itemIndex, level) => {\n const item = this._getLevelItems(level)[itemIndex]\n if (item?.matches('web-ui-dropdown-item')) {\n item.setAttribute('active', '')\n }\n })\n }\n\n private _getLevelItems(level: number): HTMLElement[] {\n const content = this._overlays.get(level)?.content\n return content ? getMenuChildren(content) : []\n }\n\n private _populateLevel0() {\n const content = this._overlays.get(0)?.content\n if (!content) return\n // 初次打开即建立 marker 锚:宿主此后始终持有模板序骨架,打开期框架插入与\n // 关闭归还都以它为基准(机制与 context-menu 共用,见 menu-tree)。\n reconcileManagedMenuItems(this, content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n orderManagedMenuItems(this, content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n }\n\n private _scheduleMenuSync() {\n if (this._menuSyncScheduled) return\n this._menuSyncScheduled = true\n requestAnimationFrame(() => {\n this._menuSyncScheduled = false\n if (!this.open) return\n this._syncOpenMenuContent()\n })\n }\n\n // 打开期的统一内容同步:把打开期间框架新增的菜单子树迁入对应层级面板。\n // 一级项走 marker 锚 reconcile(保序 + 关闭按模板位归还);面板尺寸变化由\n // defineOverlay 的 autoUpdate 跟进重定位。\n private _syncOpenMenuContent() {\n const level0Content = this._overlays.get(0)?.content\n // 迁移前隐藏宿主中新项的嵌套子项(hidden slot 属性随元素一起迁移)。\n this._hideAllSubmenuChildren()\n if (level0Content) {\n reconcileManagedMenuItems(this, level0Content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n // 新迁入项的嵌套子项未打 hidden slot,会经 dropdown-item 默认 slot 渲染进\n // 父项 label 叠到菜单上;面板项在面板内渲染,宿主查询够不到,须对 content 补隐藏。\n hideNestedMenuChildren(level0Content, `${SLOT_PREFIX}-hidden`)\n // 框架注释锚点复位独立于元素重排:锚点漂移不收敛会破坏后续翻转的插入点(同 context-menu)。\n const frameworkAnchors = captureFrameworkAnchors(level0Content, MENU_ITEM_MARKER)\n orderManagedMenuItems(this, level0Content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n restoreFrameworkAnchors(level0Content, frameworkAnchors)\n }\n for (const [level, overlay] of this._overlays) {\n if (level === 0) continue\n const parentItem = this._getLevelItems(level - 1)[this._activePath[level - 1]]\n if (parentItem) moveMenuChildren(parentItem, overlay.content)\n hideNestedMenuChildren(overlay.content, `${SLOT_PREFIX}-hidden`)\n }\n this._hoverBinder.bind()\n }\n\n private _returnLevel0Items() {\n const content = this._overlays.get(0)?.content\n if (content) returnManagedMenuItemsToSlot(this, content, this._level0ItemAnchors)\n }\n\n private _populateOverlay(level: number, submenuItem: HTMLElement) {\n const content = this._overlays.get(level)?.content\n if (content) moveMenuChildren(submenuItem, content)\n }\n\n private _depopulateOverlay(level: number, parentItem?: HTMLElement) {\n const content = this._overlays.get(level)?.content\n if (!content) return\n const targetLevel = level - 1\n const submenuItem = parentItem ?? this._getLevelItems(targetLevel)[this._activePath[targetLevel]]\n if (submenuItem) moveMenuChildren(content, submenuItem)\n }\n\n /*\n * 登记与「面板是否本帧新建」解耦,与 context-menu 的根层修复同构。\n *\n * 退场被打断时(`_closeRootAfterPresence` 在 `hideOverlayPresence` 返回 false 后提前\n * return)面板仍在 DOM 里,而关闭分支已经把句柄撤了。此时重开走的是 `_ensureOverlay`\n * 的两条复用分支,它们不会重跑 `_buildOverlay`——原先只有 `_buildOverlay` 会 claim/adopt,\n * 于是出现「面板可见 + `open === true` 却没有任何登记」:Escape 关不掉它(仲裁看不到它),\n * 守卫里 `contains` 恒 false,点面板内部反被当成外部点击关掉整张菜单。\n *\n * `open` 属性路径(`updated()` 里 scheduleFrame 到 `_ensureOverlay(0)`)与 `openMenu()`\n * 都落在复用分支上,因此**所有**根 claim 都必须走这里 —— 只在 `_ensureOverlay` 里补 claim\n * 是不够的:`openMenu()` 会先把句柄建好,使那次补 claim 变成 no-op,子层照样被漏掉。\n *\n * 新 claim 即新会话,旧会话连同其全部 adopt 子树一起作废,因此**仍在场的**子层必须\n * 在这里统一重挂。来源有两个:`_overlays` 里登记的层级,以及本轮已退场、被移出\n * `_overlays` 却仍在 DOM 里的收尾栈面板 —— 只看前者会漏掉后者(见下)。\n */\n private _claimRoot(panel: HTMLElement): OpenOverlayHandle {\n this._handle?.release()\n this._handle = this._overlay.claim(panel)\n for (const [level, sub] of this._overlays) {\n if (level === 0) continue\n sub.handle = this._handle.adopt(sub.overlay)\n }\n /*\n * 收尾栈里的子菜单面板同样「可见但已脱离快照」:`_closeSubmenuFrom` 只把它们移出\n * `_overlays`,层仍挂在旧句柄下、面板仍在 DOM 里退场。不重挂的话它们会被判成面板外,\n * 点它内部就关掉整张菜单 —— base 的 `composition.contains` 走登记树、且退场期间不注销,\n * 所以这条是相对 base 的行为回归,不是 parity。\n * 新句柄要写回容器:收尾结束时 adapter.dispose 释放的是 `container.handle`。\n */\n for (const container of this._closingSubmenus.closing()) {\n container.handle = this._handle.adopt(container.overlay)\n }\n return this._handle\n }\n\n /** 句柄缺失时补登记;面板还没建好时以宿主代填,与 `_openMenu` 的占位会话同一口径。 */\n private _ensureRootHandle(): OpenOverlayHandle {\n return this._handle ?? this._claimRoot(this._overlays.get(0)?.overlay ?? this)\n }\n\n private _ensureOverlay(level: number, isInstant = false, submenuItem?: HTMLElement): boolean {\n const closingOverlay = submenuItem ? this._closingSubmenus.take(submenuItem) : undefined\n if (closingOverlay) {\n // 从 closing 栈取回的面板:登记已在关闭时撤销,这里必须重新挂回根句柄子树。\n closingOverlay.handle = this._ensureRootHandle().adopt(closingOverlay.overlay)\n this._overlays.set(level, closingOverlay)\n closingOverlay.api.open()\n showOverlayPresence(closingOverlay.overlay, { isInstant })\n return true\n }\n const existing = this._overlays.get(level)\n if (existing) {\n this._ensureRootHandle()\n existing.api.open()\n showOverlayPresence(existing.overlay, { isInstant })\n return false\n }\n this._buildOverlay(level, isInstant)\n return false\n }\n\n private _buildOverlay(level: number, isInstant = false) {\n const { panel: overlay, content } = createMenuPortalOverlay('dropdown-overlay', this)\n overlay.setAttribute('role', 'menu')\n // 根层级面板带 id:trigger 的 aria-controls 指向它(Q9a)。\n if (level === 0) overlay.id = `wui-dropdown-menu-${++dropdownIdCounter}`\n overlay.dataset.level = String(level)\n overlay.addEventListener('click', this._onMenuClick)\n overlay.addEventListener('keydown', this._onKeydown)\n\n const anchor = level === 0 ? this._queryTriggerAnchor() : this._getSubmenuTriggerAnchor(level - 1)\n\n if (anchor) {\n const ctrl = defineOverlay().make({\n anchor,\n overlay,\n placement: level === 0 ? this.placement : 'right-start',\n offset: level === 0 ? this.offset : 0,\n // 菜单至少与 trigger/父项同宽,内容可更宽(floor 来自 --wui-overlay-min-width)\n minAnchorWidth: true,\n matchWidth: level === 0 ? this.matchWidth : false,\n strategy: 'fixed'\n })\n if (level === 0) {\n // 面板就绪:释放「以宿主代填」的占位会话,换成真实面板。\n this._claimRoot(overlay)\n this._overlays.set(level, { api: ctrl, overlay, content })\n this._populateLevel0()\n } else {\n /*\n * 子层进根句柄子树,默认不是独立候选 —— 「在树里但不是这一层」由此表达;\n * 原先按 level 找父面板的判断随之消失。用 `_ensureRootHandle()` 而不是 `_handle?.`:\n * 后者在句柄意外缺失时会静默跳过 adopt,留下一棵无人认领的子树。\n */\n const handle = this._ensureRootHandle().adopt(overlay)\n this._overlays.set(level, { api: ctrl, overlay, content, handle })\n }\n // submenu 父项在面板 content 内,其新增子项只有观察 content 子树才能看到\n this._menuContentObserver.observe(content, { childList: true, subtree: true })\n ctrl.open()\n showOverlayPresence(overlay, { isInstant })\n }\n }\n\n private _disposeOverlay(level: number) {\n const overlay = this._overlays.get(level)\n if (level === 0) {\n // 根句柄撤销会递归撤销全部已 adopt 的子层。\n this._handle?.release()\n this._handle = null\n }\n // 子层句柄随所属 MenuOverlay 释放;已被根层递归撤销时幂等无操作。\n overlay?.handle?.release()\n overlay?.overlay.removeEventListener('click', this._onMenuClick)\n overlay?.overlay.removeEventListener('keydown', this._onKeydown)\n overlay?.overlay.remove()\n overlay?.api.dispose()\n this._overlays.delete(level)\n this._rebindMenuContentObservers()\n }\n\n // jsdom 的 MutationObserver 不实现 unobserve;统一 disconnect 后重绑剩余目标。\n private _rebindMenuContentObservers() {\n this._menuContentObserver.disconnect()\n this._menuContentObserver.observe(this, { childList: true, subtree: true })\n for (const overlay of this._overlays.values()) {\n this._menuContentObserver.observe(overlay.content, { childList: true, subtree: true })\n }\n }\n\n private _disposeAll() {\n /*\n * 先无条件撤销根句柄。`_openMenu` 会先用宿主当 panel claim 一次占位会话(真实面板要等\n * 一帧才建好),若这段时间内宿主被卸载、`_buildOverlay` 没机会跑,`_overlays` 仍是空的\n * —— 只遍历 `_overlays` 就会漏掉它:该层永久留在模块注册表里强引用整个组件,且宿主\n * 重挂载后 `panel.isConnected` 又为真,占位层重新成为候选、吞掉 Escape。\n */\n this._handle?.release()\n this._handle = null\n for (const level of this._overlays.keys()) {\n this._disposeOverlay(level)\n }\n }\n\n private _syncScrollLock(isOpen = this.open) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n private _queryTriggerAnchor(): HTMLElement | null {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n const el = slot?.assignedElements()[0]\n return el instanceof HTMLElement ? el : null\n }\n\n /*\n * trigger 语义回写(Q9a):slot 进来的元素是真正的可聚焦交互元素,\n * aria-haspopup / aria-expanded / aria-controls 必须设在它身上;\n * 组件没有 trigger 包装 div,此前完全没有 ARIA 接线。\n */\n private _syncTriggerAria() {\n const trigger = this._queryTriggerAnchor()\n if (!trigger) return\n trigger.setAttribute('aria-haspopup', 'menu')\n trigger.setAttribute('aria-expanded', String(this.open))\n const rootOverlay = this._overlays.get(0)?.overlay\n if (rootOverlay?.id) trigger.setAttribute('aria-controls', rootOverlay.id)\n else trigger.removeAttribute('aria-controls')\n }\n\n private _getSubmenuTriggerAnchor(level: number): HTMLElement | null {\n const idx = this._activePath[level]\n if (idx === undefined) return null\n const items = this._getLevelItems(level)\n const item = items[idx]\n if (!item) return null\n return item.shadowRoot?.querySelector('.item-inner') ?? item\n }\n\n private _onTriggerClick = (event: MouseEvent) => {\n if (this.disabled) return\n if (this.open) {\n this._closeAll(true)\n } else {\n /*\n * `detail === 0` ⇔ 键盘激活(Enter/Space 触发的 click),`detail > 0` ⇔ 指针点击。\n * 同一个信号既决定是否免入场过渡,也决定首项要不要留 `:focus-visible`:\n * 键盘打开首项即应有 accent 焦点视觉,指针打开则抑制(见 `_suppressInitialFocusVisible`)。\n */\n const fromKeyboard = event.detail === 0\n this._openMenu(fromKeyboard, true, !fromKeyboard)\n }\n }\n\n private _onClickOutside = (e: MouseEvent) => {\n if (!this.isOpen || this._outsideClickGuard.isArmed()) return\n if (this._outsideClickGuard.isInside(e)) return\n this._closeAll(true)\n }\n\n private _onMenuClick = (e: MouseEvent) => {\n const item = getMenuItemFromEvent(e)\n if (!item || item.hasAttribute('disabled') || !item.hasAttribute('submenu')) return\n\n const level = this._getFocusedLevel(item)\n if (level === undefined) return\n const itemIndex = this._getLevelItems(level).indexOf(item)\n if (itemIndex >= 0) this._toggleSubmenu(level, itemIndex, e.detail === 0)\n }\n\n private _onKeydown = (e: KeyboardEvent) => {\n if (this.disabled || !this.open) return\n\n // overlay 上键盘事件 target 是菜单项而非组件自身,不能限定 e.target === this,\n // 否则鼠标打开、焦点在菜单项时 Escape 无法关闭\n handleMenuKeyboard(this._keyboardDelegate, e)\n }\n\n private _getFocusedLevel(item?: HTMLElement): number | undefined {\n if (!item) return undefined\n const overlay = [...this._overlays.entries()].find(([, value]) => value.overlay.contains(item))\n return overlay?.[0]\n }\n\n private _bindHoversAfterUpdate() {\n requestAnimationFrame(() => this._hoverBinder.bind())\n }\n\n override render() {\n return html`\n <div class=\"dropdown-trigger\" @click=${this._onTriggerClick}>\n <slot name=\"trigger\"></slot>\n </div>\n `\n }\n\n declare readonly $events: {\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-dropdown': WebUiDropdown\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  //#region src/components/dropdown-divider/style.css?inline
2
- var e = "*,:before,:after{box-sizing:border-box}:host{-webkit-user-select:none;user-select:none;display:block}.divider{background-color:var(--wui-color-border,#0000001a);height:1px;margin:4px 8px}";
2
+ var e = "*,:before,:after{box-sizing:border-box}:host{-webkit-user-select:none;user-select:none;display:block}.divider{height:1px;margin:var(--wui-space-1,4px) var(--wui-space-2,8px);background-color:var(--wui-color-border,#0000001a)}";
3
3
  //#endregion
4
4
  export { e as default };
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/dropdown-divider/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n user-select: none;\n display: block;\n}\n\n.divider {\n height: 1px;\n margin: 4px 8px;\n background-color: var(--wui-color-border, rgb(0 0 0 / 0.1));\n}\n"],"mappings":""}
1
+ {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/dropdown-divider/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n user-select: none;\n display: block;\n}\n\n.divider {\n height: 1px;\n margin: var(--wui-space-1, 4px) var(--wui-space-2, 8px);\n background-color: var(--wui-color-border, rgb(0 0 0 / 0.1));\n}\n"],"mappings":""}
@@ -1,5 +1,5 @@
1
1
  //#region src/components/dropdown-header/style.css?inline
2
- var e = "*,:before,:after{box-sizing:border-box}:host{display:block}.header{-webkit-user-select:none;user-select:none;color:var(--wui-color-text-tertiary,color-mix(in srgb, var(--wui-color-text) 35%, transparent));padding:8px 8px 4px;font-size:12px;font-weight:600}";
2
+ var e = "*,:before,:after{box-sizing:border-box}:host{display:block}.header{-webkit-user-select:none;user-select:none;padding:var(--wui-space-2,8px) var(--wui-space-2,8px) var(--wui-space-1,4px);font-size:var(--wui-font-size-caption,12px);font-weight:var(--wui-font-weight-semibold,600);color:var(--wui-color-text-tertiary,color-mix(in srgb, var(--wui-color-text) 35%, transparent))}";
3
3
  //#endregion
4
4
  export { e as default };
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/dropdown-header/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: block;\n}\n\n.header {\n user-select: none;\n\n padding: 8px 8px 4px;\n\n font-size: 12px;\n font-weight: 600;\n color: var(--wui-color-text-tertiary, color-mix(in srgb, var(--wui-color-text) 35%, transparent));\n}\n"],"mappings":""}
1
+ {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/dropdown-header/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: block;\n}\n\n.header {\n user-select: none;\n\n padding: var(--wui-space-2, 8px) var(--wui-space-2, 8px) var(--wui-space-1, 4px);\n\n font-size: var(--wui-font-size-caption, 12px);\n font-weight: var(--wui-font-weight-semibold, 600);\n color: var(--wui-color-text-tertiary, color-mix(in srgb, var(--wui-color-text) 35%, transparent));\n}\n"],"mappings":""}
@@ -1,5 +1,5 @@
1
1
  //#region src/components/dropdown-item/style.css?inline
2
- var e = "*,:before,:after{box-sizing:border-box}:host{display:block}:host([disabled]){cursor:not-allowed;opacity:.4}.item-inner{-webkit-user-select:none;user-select:none;border-radius:var(--wui-radius-control,calc(infinity * 1px));height:32px;font-size:var(--wui-font-size,14px);color:inherit;white-space:nowrap;outline:none;align-items:center;gap:8px;padding:0 8px;display:flex}.item-inner:focus-visible{color:var(--wui-color-on-accent,#fff);background:var(--wui-color-accent,#08f)}:host([active]) .item-inner{color:inherit;background:var(--wui-color-border,#0000001a)}@media (hover:hover) and (pointer:fine){:host(:hover) .item-inner{color:var(--wui-color-on-accent,#fff);background:var(--wui-color-accent,#08f)}}.item-label{min-width:0;color:inherit;text-overflow:ellipsis;flex:1;overflow:hidden}.item-suffix{opacity:.4;flex:none}";
2
+ var e = "*,:before,:after{box-sizing:border-box}:host{display:block}:host([disabled]){cursor:not-allowed;opacity:.4}.item-inner{-webkit-user-select:none;user-select:none;gap:var(--wui-space-2,8px);height:32px;padding:0 var(--wui-space-2,8px);border-radius:var(--wui-radius-control,calc(infinity * 1px));font-size:var(--wui-font-size,14px);color:inherit;white-space:nowrap;outline:none;align-items:center;display:flex}.item-inner:focus-visible{color:var(--wui-color-on-accent,#fff);background:var(--wui-color-accent,#08f)}:host([active]) .item-inner{color:inherit;background:var(--wui-color-border,#0000001a)}:host([data-wui-menu-focus-suppressed]) .item-inner:focus-visible{color:inherit;background:0 0}@media (hover:hover) and (pointer:fine){:host([data-wui-menu-focus-suppressed]) .item-inner:hover,:host(:not([active])) .item-inner:hover:not(:focus-visible){color:inherit;background:var(--wui-color-border,#0000001a)}}.item-label{min-width:0;color:inherit;text-overflow:ellipsis;flex:1;overflow:hidden}.item-suffix{opacity:.4;flex:none}";
3
3
  //#endregion
4
4
  export { e as default };
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/dropdown-item/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: block;\n}\n\n:host([disabled]) {\n cursor: not-allowed;\n opacity: 0.4;\n}\n\n.item-inner {\n user-select: none;\n\n display: flex;\n gap: 8px;\n align-items: center;\n\n height: 32px;\n padding: 0 8px;\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n\n font-size: var(--wui-font-size, 14px);\n color: inherit;\n white-space: nowrap;\n\n outline: none;\n}\n\n.item-inner:focus-visible {\n color: var(--wui-color-on-accent, #fff);\n background: var(--wui-color-accent, #08f);\n}\n\n:host([active]) .item-inner {\n color: inherit;\n background: var(--wui-color-border, rgb(0 0 0 / 0.1));\n}\n\n@media (hover: hover) and (pointer: fine) {\n :host(:hover) .item-inner {\n color: var(--wui-color-on-accent, #fff);\n background: var(--wui-color-accent, #08f);\n }\n}\n\n.item-label {\n overflow: hidden;\n flex: 1;\n\n min-width: 0;\n\n color: inherit;\n text-overflow: ellipsis;\n}\n\n.item-suffix {\n flex: none;\n opacity: 0.4;\n}\n"],"mappings":""}
1
+ {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/dropdown-item/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: block;\n}\n\n:host([disabled]) {\n cursor: not-allowed;\n opacity: 0.4;\n}\n\n.item-inner {\n user-select: none;\n\n display: flex;\n gap: var(--wui-space-2, 8px);\n align-items: center;\n\n height: 32px;\n padding: 0 var(--wui-space-2, 8px);\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n\n font-size: var(--wui-font-size, 14px);\n color: inherit;\n white-space: nowrap;\n\n outline: none;\n}\n\n.item-inner:focus-visible {\n color: var(--wui-color-on-accent, #fff);\n background: var(--wui-color-accent, #08f);\n}\n\n:host([active]) .item-inner {\n color: inherit;\n background: var(--wui-color-border, rgb(0 0 0 / 0.1));\n}\n\n:host([data-wui-menu-focus-suppressed]) .item-inner:focus-visible {\n color: inherit;\n background: transparent;\n}\n\n@media (hover: hover) and (pointer: fine) {\n :host([data-wui-menu-focus-suppressed]) .item-inner:hover,\n :host(:not([active])) .item-inner:hover:not(:focus-visible) {\n color: inherit;\n background: var(--wui-color-border, rgb(0 0 0 / 0.1));\n }\n}\n\n.item-label {\n overflow: hidden;\n flex: 1;\n\n min-width: 0;\n\n color: inherit;\n text-overflow: ellipsis;\n}\n\n.item-suffix {\n flex: none;\n opacity: 0.4;\n}\n"],"mappings":""}
@@ -368,6 +368,46 @@ export declare class WebUiEditableText extends WebUiEditableText_base {
368
368
  name: string;
369
369
  placeholder: string;
370
370
  disabled: boolean;
371
+ readonly: boolean;
372
+ /**
373
+ * 卸载时按提交语义结束编辑,默认 false。
374
+ *
375
+ * 跨引擎实测(Chromium 与 WebKit 各一轮)结论**不一致**,这是本属性的主要价值来源:
376
+ *
377
+ * - Chromium:移除正在编辑的组件会向编辑层派发 `blur`,默认行为下 `change` 已照常派发
378
+ * 一次,草稿不丢。blur 与 change 同步排在 `disconnectedCallback` 之后(后者在
379
+ * disconnected 步骤内跑完),卸载因此没拿到一条「排在该消费方状态更新之前」的提交来源。
380
+ * - WebKit(Safari / iOS):移除**不派发 blur**,默认路径什么都不提交,草稿随组件一起丢弃。
381
+ * 这里 blur 兜底不存在,只有显式开启本属性才能保住草稿。
382
+ * - Gecko(Firefox)**未验证**:实测所用机器上没有任何 Firefox 能启动,这里不做断言。把它当作
383
+ * 未知,而不是默认与上面两者之一相同。
384
+ *
385
+ * 所以在 Safari 上这不是可选优化而是必需的:默认值 `false` 对 Safari 用户等于没有兜底,
386
+ * 消费方若依赖「行被回收时草稿不丢」,在 Safari 上必须显式开启本属性。
387
+ *
388
+ * 开启后由 `disconnectedCallback` 复用 `_commitEditing`,与 `Enter`/`blur` 是同一份实现,
389
+ * 卸载不是第三条独立的提交语义。副作用与 `blur` 提交一致:草稿成为新值、退出编辑、
390
+ * 派发一次 `change`(冒泡且组合)。两处与 `blur` 刻意不同:
391
+ *
392
+ * 1. 提交判据——`Enter` 与 `blur` 由用户动作触发,一律算提交意图;卸载没有这种意图,
393
+ * 因此只在草稿与进入编辑时的值确实不同时才提交,不产生空提交。
394
+ * 2. 禁用与只读态——`disabled` / `readonly` 时与 `_onBlur` 一致:不派发提交事件。两者对「已有
395
+ * 草稿」的后果不同,必须分开说:`disabled` 下草稿保留在组件上;`readonly` 下草稿是真的
396
+ * 丢掉——`readonly` 阻止不了先前的输入,而卸载时不会再有 blur 兜底(WebKit 上连 blur
397
+ * 都没有)。可达路径是「可编辑态进入 → 起草稿 → 期间翻 readonly → 卸载」。这里不按取消
398
+ * 处理:cancel 的契约是恢复 `_editBase`,而卸载既没有焦点要交还也没有宿主接收结果,
399
+ * 改成 cancel 还会与「与 `_onBlur` 一致」这条公开承诺相悖。用户看到的是「我打的字没了」,
400
+ * 既没有 change 也没有 cancel。
401
+ *
402
+ * 派发发生在卸载回调内,`change` 沿组合路径向上派发,因此挂在组件自身或任一祖先上的监听
403
+ * 都会执行,**包括已经随宿主一起被卸载的祖先**——派发不要求祖先仍在文档中。这正是虚拟化
404
+ * 场景需要的:消费方的监听器往往就挂在被回收的那一行上。因此监听器不能假定组件此刻仍在
405
+ * 文档里;需要把结果交给宿主树之外的状态时,用 `queueMicrotask` 之类的显式转交。
406
+ *
407
+ * 祖先上的 `display: none` 不是卸载:组件仍在文档中、不派发任何事件(两引擎一致),那种
408
+ * 场合需要消费方主动结束编辑。
409
+ */
410
+ commitOnUnmount: boolean;
371
411
  ariaLabel: string | null;
372
412
  private _value;
373
413
  /**
@@ -383,6 +423,7 @@ export declare class WebUiEditableText extends WebUiEditableText_base {
383
423
  private _pendingCaret;
384
424
  private _refocusing;
385
425
  private _resizeObserver;
426
+ private readonly _fieldId;
386
427
  private get _isDisabled();
387
428
  private readonly _formAssociation;
388
429
  private readonly _formAssociationController;
@@ -1,21 +1,22 @@
1
1
  import e from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
2
2
  import { FormAssociated as t, FormAssociationController as n, defineFormAssociation as r } from "../../shared/form-association/index.js";
3
- import i from "./style.js";
4
- import { LitElement as a, html as o, unsafeCSS as s } from "lit";
5
- import { customElement as c, property as l, query as u, state as d } from "lit/decorators.js";
6
- import { ifDefined as f } from "lit/directives/if-defined.js";
7
- import { classMap as p } from "lit/directives/class-map.js";
3
+ import { createFieldId as i } from "../../shared/form-association/field-id.js";
4
+ import a from "./style.js";
5
+ import { LitElement as o, html as s, unsafeCSS as c } from "lit";
6
+ import { customElement as l, property as u, query as d, state as f } from "lit/decorators.js";
7
+ import { ifDefined as p } from "lit/directives/if-defined.js";
8
+ import { classMap as m } from "lit/directives/class-map.js";
8
9
  //#region src/components/editable-text/index.ts
9
- var m = "​", h = 1;
10
- function g(e) {
10
+ var h = "​", g = 1;
11
+ function _(e) {
11
12
  for (let t of e.childNodes) if (t instanceof Text) return t;
12
13
  return null;
13
14
  }
14
- function _(e, t, n) {
15
- let r = g(e);
15
+ function v(e, t, n) {
16
+ let r = _(e);
16
17
  if (!r) return 0;
17
18
  let i = r.data.length, a = document.createRange(), o = (e) => (a.setStart(r, e), a.setEnd(r, e), a.getBoundingClientRect()), s = [];
18
- for (let t of e.getClientRects()) s.some((e) => Math.abs(e.top - t.top) <= h) || s.push(t);
19
+ for (let t of e.getClientRects()) s.some((e) => Math.abs(e.top - t.top) <= g) || s.push(t);
19
20
  if (s.length === 0) return i;
20
21
  let c = s[0], l = Infinity;
21
22
  for (let e of s) {
@@ -25,23 +26,23 @@ function _(e, t, n) {
25
26
  let u = 0, d = i;
26
27
  for (; u < d;) {
27
28
  let e = u + d >> 1;
28
- o(e).top >= c.top - h ? d = e : u = e + 1;
29
+ o(e).top >= c.top - g ? d = e : u = e + 1;
29
30
  }
30
31
  let f = u;
31
32
  for (u = f, d = i; u < d;) {
32
33
  let e = u + d + 1 >> 1;
33
- o(e).top <= c.top + h ? u = e : d = e - 1;
34
+ o(e).top <= c.top + g ? u = e : d = e - 1;
34
35
  }
35
36
  let p = Math.max(u, f);
36
37
  for (u = f, d = p; u < d;) {
37
38
  let e = u + d + 1 >> 1;
38
- o(e).left <= t + h ? u = e : d = e - 1;
39
+ o(e).left <= t + g ? u = e : d = e - 1;
39
40
  }
40
41
  return u;
41
42
  }
42
- var v = class extends t(a) {
43
+ var y = class extends t(o) {
43
44
  static {
44
- this.styles = [s(i)];
45
+ this.styles = [c(a)];
45
46
  }
46
47
  get value() {
47
48
  return this._value;
@@ -54,7 +55,7 @@ var v = class extends t(a) {
54
55
  return this.disabled || this._formAssociation.isFormDisabled();
55
56
  }
56
57
  constructor() {
57
- super(), this.name = "", this.placeholder = "", this.disabled = !1, this.ariaLabel = null, this._value = "", this._editing = !1, this._editBase = "", this._pendingCaret = null, this._refocusing = !1, this._resizeObserver = null, this._formAssociation = r({
58
+ super(), this.name = "", this.placeholder = "", this.disabled = !1, this.readonly = !1, this.commitOnUnmount = !1, this.ariaLabel = null, this._value = "", this._editing = !1, this._editBase = "", this._pendingCaret = null, this._refocusing = !1, this._resizeObserver = null, this._fieldId = i("web-ui-editable-text"), this._formAssociation = r({
58
59
  host: this,
59
60
  getState: () => this._value,
60
61
  setState: (e) => {
@@ -71,12 +72,15 @@ var v = class extends t(a) {
71
72
  e.preventDefault(), e.stopPropagation(), this._cancelEditing(), this._returnFocusToHost();
72
73
  return;
73
74
  }
74
- e.key === "Enter" && (e.preventDefault(), e.stopPropagation(), this._commitEditing(!0));
75
+ if (e.key === "Enter") {
76
+ if (e.preventDefault(), e.stopPropagation(), this.readonly) return;
77
+ this._commitEditing(!0);
78
+ }
75
79
  }
76
80
  }, this.addEventListener("focus", this._onHostFocus);
77
81
  }
78
82
  disconnectedCallback() {
79
- super.disconnectedCallback(), this._releaseEditingKeys(), this._teardownAutosize();
83
+ super.disconnectedCallback(), this.commitOnUnmount && this._editing && !this._isDisabled && !this.readonly && this._value !== this._editBase && this._commitEditing(!1), this._releaseEditingKeys(), this._teardownAutosize();
80
84
  }
81
85
  connectedCallback() {
82
86
  super.connectedCallback(), this._setupAutosize(), this._editing && this._claimEditingKeys();
@@ -92,7 +96,7 @@ var v = class extends t(a) {
92
96
  this._enterEditing();
93
97
  }
94
98
  _onPointerDown(e) {
95
- this._isDisabled || this._editing || !this._textLayer || (this._pendingCaret = _(this._textLayer, e.clientX, e.clientY));
99
+ this._isDisabled || this._editing || !this._textLayer || (this._pendingCaret = v(this._textLayer, e.clientX, e.clientY));
96
100
  }
97
101
  _onClick() {
98
102
  this._isDisabled || this._editing || this._enterEditing();
@@ -118,10 +122,21 @@ var v = class extends t(a) {
118
122
  this._editing && (this._editing = !1, this._releaseEditingKeys(), this.toggleAttribute("editing", !1));
119
123
  }
120
124
  _onInput() {
121
- this._value = this._editor?.value ?? "", this._formAssociation.sync(), this._autosizeEditor();
125
+ let e = this._editor;
126
+ if (this._isDisabled || this.readonly) {
127
+ e && e.value !== this._value && (e.value = this._value);
128
+ return;
129
+ }
130
+ this._value = e?.value ?? "", this._formAssociation.sync(), this._autosizeEditor();
122
131
  }
123
132
  _onBlur() {
124
- this._editing && !this._isDisabled && this._commitEditing(!1);
133
+ if (this._editing && !this._isDisabled) {
134
+ if (this.readonly) {
135
+ this._exitEditing();
136
+ return;
137
+ }
138
+ this._commitEditing(!1);
139
+ }
125
140
  }
126
141
  _commitEditing(e) {
127
142
  this._editing && (this._exitEditing(), e && this._returnFocusToHost(), this.dispatchEvent(new Event("change", {
@@ -165,21 +180,23 @@ var v = class extends t(a) {
165
180
  e.style.height = n > 0 ? `${n}px` : t;
166
181
  }
167
182
  get _displayText() {
168
- return this._value === "" ? this.placeholder === "" ? m : this.placeholder : this._value;
183
+ return this._value === "" ? this.placeholder === "" ? h : this.placeholder : this._value;
169
184
  }
170
185
  render() {
171
- return o`
186
+ return s`
172
187
  <div class="layers" @pointerdown=${this._onPointerDown} @click=${this._onClick}>
173
- <span class=${p({
188
+ <span class=${m({
174
189
  text: !0,
175
190
  placeholder: this._value === ""
176
191
  })}>${this._displayText}</span>
177
192
  <textarea
193
+ id=${this._fieldId}
178
194
  class="editor"
179
195
  .value=${this._value}
180
196
  .placeholder=${this.placeholder}
181
197
  ?disabled=${this._isDisabled}
182
- aria-label=${f(this.ariaLabel)}
198
+ ?readonly=${this.readonly}
199
+ aria-label=${p(this.ariaLabel)}
183
200
  @input=${this._onInput}
184
201
  @blur=${this._onBlur}
185
202
  ></textarea>
@@ -187,20 +204,27 @@ var v = class extends t(a) {
187
204
  `;
188
205
  }
189
206
  };
190
- e([l({
207
+ e([u({
191
208
  type: String,
192
209
  reflect: !0
193
- })], v.prototype, "name", void 0), e([l({
210
+ })], y.prototype, "name", void 0), e([u({
194
211
  type: String,
195
212
  reflect: !0
196
- })], v.prototype, "placeholder", void 0), e([l({
213
+ })], y.prototype, "placeholder", void 0), e([u({
214
+ type: Boolean,
215
+ reflect: !0
216
+ })], y.prototype, "disabled", void 0), e([u({
197
217
  type: Boolean,
198
218
  reflect: !0
199
- })], v.prototype, "disabled", void 0), e([l({
219
+ })], y.prototype, "readonly", void 0), e([u({
220
+ type: Boolean,
221
+ reflect: !0,
222
+ attribute: "commit-on-unmount"
223
+ })], y.prototype, "commitOnUnmount", void 0), e([u({
200
224
  type: String,
201
225
  attribute: "aria-label"
202
- })], v.prototype, "ariaLabel", void 0), e([d()], v.prototype, "_value", void 0), e([l({ type: String })], v.prototype, "value", null), e([u(".editor")], v.prototype, "_editor", void 0), e([u(".text")], v.prototype, "_textLayer", void 0), v = e([c("web-ui-editable-text")], v);
226
+ })], y.prototype, "ariaLabel", void 0), e([f()], y.prototype, "_value", void 0), e([u({ type: String })], y.prototype, "value", null), e([d(".editor")], y.prototype, "_editor", void 0), e([d(".text")], y.prototype, "_textLayer", void 0), y = e([l("web-ui-editable-text")], y);
203
227
  //#endregion
204
- export { v as WebUiEditableText };
228
+ export { y as WebUiEditableText };
205
229
 
206
230
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/editable-text/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\nimport { ifDefined } from 'lit/directives/if-defined.js'\n\nimport { FormAssociated, defineFormAssociation, FormAssociationController } from '@/shared/form-association'\n\nimport style from './style.css?inline'\n\n/** 空值且无 placeholder 时占位,保留一个行盒,避免宿主退化成 0 高度、无法点击进入编辑。 */\nconst BLANK_GLYPH = '\\u200B'\n\n/** 行/字符落点二分的容差(px),吸收次像素与舍入差异。 */\nconst CARET_TOLERANCE = 1\n\n/**\n * 取文本层承载文案的 Text 节点。\n *\n * 绑定表达式位于元素首个位置时,Lit 会在它前面留一个注释标记节点,因此不能用\n * `firstChild`:那会拿到没有布局的注释,Range 量不到任何矩形。\n */\nfunction textNodeOf(layer: HTMLElement): Text | null {\n for (const child of layer.childNodes) {\n if (child instanceof Text) return child\n }\n return null\n}\n\n/**\n * 把视口坐标映射为文本层的字符偏移。\n *\n * `document.caretPositionFromPoint` 不进入 shadow DOM(对 shadow 内容返回 shadow host\n * 之外的节点),因此在文本层上用 Range 空矩形二分:先按 y 选行,再在行内按 x 选字符。\n * 文本层与编辑层同盒同排版,偏移与 textarea 的 selectionStart 一一对应。\n */\nfunction caretOffsetFromPoint(textLayer: HTMLElement, x: number, y: number): number {\n const node = textNodeOf(textLayer)\n if (!node) return 0\n const length = node.data.length\n const range = document.createRange()\n const rectAt = (offset: number): DOMRect => {\n range.setStart(node, offset)\n range.setEnd(node, offset)\n return range.getBoundingClientRect()\n }\n\n // 同一行的盒可能被拆成多个 client rect;按 top 去重得到行列表,取与 y 最近的一行,\n // 距离相等时取更靠下的一行(与浏览器在行间的落点行为一致)。\n const lines: DOMRect[] = []\n for (const rect of textLayer.getClientRects()) {\n if (!lines.some(line => Math.abs(line.top - rect.top) <= CARET_TOLERANCE)) lines.push(rect)\n }\n if (lines.length === 0) return length\n let line = lines[0]\n let nearest = Infinity\n for (const candidate of lines) {\n const distance = Math.abs((candidate.top + candidate.bottom) / 2 - y)\n if (distance <= nearest) {\n nearest = distance\n line = candidate\n }\n }\n\n // 行首:caret top 首次落入本行的最小偏移\n let lo = 0\n let hi = length\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if (rectAt(mid).top >= line.top - CARET_TOLERANCE) hi = mid\n else lo = mid + 1\n }\n const lineStart = lo\n // 行尾:caret top 仍落在本行的最大偏移\n lo = lineStart\n hi = length\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1\n if (rectAt(mid).top <= line.top + CARET_TOLERANCE) lo = mid\n else hi = mid - 1\n }\n const lineEnd = Math.max(lo, lineStart)\n // 行内:caret left 不超过 x 的最大偏移\n lo = lineStart\n hi = lineEnd\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1\n if (rectAt(mid).left <= x + CARET_TOLERANCE) lo = mid\n else hi = mid - 1\n }\n return lo\n}\n\n@customElement('web-ui-editable-text')\nexport class WebUiEditableText extends FormAssociated(LitElement) {\n static override styles = [unsafeCSS(style)]\n\n @property({ type: String, reflect: true }) name = ''\n @property({ type: String, reflect: true }) placeholder = ''\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: String, attribute: 'aria-label' }) override ariaLabel: string | null = null\n\n @state() private _value = ''\n\n /**\n * `value` attribute 承载 form reset 的初值,不随当前值反射;property 暴露当前值,\n * 与原生 input 的 value / defaultValue 关系一致。\n */\n @property({ type: String })\n get value(): string {\n return this._value\n }\n\n set value(v: string) {\n const old = this._value\n this._value = v\n this._formAssociation.sync()\n this.requestUpdate('value', old)\n }\n\n @query('.editor') private _editor?: HTMLTextAreaElement\n @query('.text') private _textLayer?: HTMLElement\n\n private _editing = false\n private _editBase = ''\n private _pendingCaret: number | null = null\n private _refocusing = false\n private _resizeObserver: ResizeObserver | null = null\n\n private get _isDisabled(): boolean {\n return this.disabled || this._formAssociation.isFormDisabled()\n }\n\n private readonly _formAssociation = defineFormAssociation<string>({\n host: this,\n getState: () => this._value,\n setState: value => {\n this.value = value\n },\n getFormValue: () => this._value,\n getFormState: () => this._value,\n restoreState: state => {\n if (typeof state === 'string') this.value = state\n }\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n constructor() {\n super()\n // focus 不冒泡:宿主监听只在焦点从组件外进入时触发,shadow 内聚焦不会误触\n this.addEventListener('focus', this._onHostFocus)\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._releaseEditingKeys()\n this._teardownAutosize()\n }\n\n override connectedCallback() {\n super.connectedCallback()\n /*\n * autosize 的 ResizeObserver 在 disconnectedCallback 被拆掉,搭建若只放在\n * firstUpdated(一生一次),元素断开再重连后 RO 永久丢失、autosize 静默失效。\n * connectedCallback 每次连接都跑:_setupAutosize 自带 teardown,重复进入无副作用。\n * 首连接时渲染尚未发生(_editor 为 null),_autosizeEditor 与 RO 回调都有判空守卫。\n */\n this._setupAutosize()\n // 同类缺口:编辑态被断开时 _releaseEditingKeys 撤了 window 监听,重连后 _editing\n // 仍为 true(blur 取消未必先于断开到达),此时补领,避免重连后按键失灵。\n if (this._editing) this._claimEditingKeys()\n }\n\n override updated() {\n // disabled 时移出 tab 序列;编辑态由 editing attribute 表达,二者互斥\n if (this._isDisabled) this.removeAttribute('tabindex')\n else this.setAttribute('tabindex', '0')\n // 禁用态生效时(属性或 fieldset)退出编辑:值保持当前草稿,不派发提交事件\n if (this._editing && this._isDisabled) this._exitEditing()\n this._autosizeEditor()\n }\n\n private _onHostFocus() {\n if (this._refocusing) {\n this._refocusing = false\n return\n }\n this._enterEditing()\n }\n\n /**\n * pointerdown 时文本层仍是命中目标,此时记录落点;浏览器随后的默认聚焦会触发\n * host focus 进入编辑,偏移在那里应用到 textarea。\n */\n private _onPointerDown(e: PointerEvent) {\n if (this._isDisabled || this._editing || !this._textLayer) return\n this._pendingCaret = caretOffsetFromPoint(this._textLayer, e.clientX, e.clientY)\n }\n\n /** mousedown 默认聚焦被拦截等场景由 click 兜底进入编辑。 */\n private _onClick() {\n if (this._isDisabled || this._editing) return\n this._enterEditing()\n }\n\n /**\n * 公共 API:进入编辑态并全选内容;已在编辑态时只重新全选。\n *\n * `disabled` 时不进入编辑,与 `focus()` 一致。\n */\n select() {\n if (this._isDisabled) return\n if (!this._editing) this._enterEditing()\n const editor = this._editor\n // 全选:末端显式取当前文本长度,覆盖 _enterEditing 落下的光标位置\n editor?.setSelectionRange(0, editor.value.length)\n }\n\n private _enterEditing() {\n if (this._isDisabled || this._editing) return\n this._editing = true\n this._editBase = this._value\n this.toggleAttribute('editing', true)\n this._claimEditingKeys()\n const editor = this._editor\n if (!editor) return\n editor.focus()\n // Tab / 编程式聚焦光标到末尾;指针落点用 pointerdown 记下的偏移\n const offset = this._pendingCaret ?? this._value.length\n this._pendingCaret = null\n const clamped = Math.max(0, Math.min(offset, editor.value.length))\n editor.setSelectionRange(clamped, clamped)\n }\n\n private _exitEditing() {\n if (!this._editing) return\n this._editing = false\n this._releaseEditingKeys()\n this.toggleAttribute('editing', false)\n }\n\n private _onInput() {\n // 原生 input 已 composed 冒泡出 shadow root 且 target 重定向到宿主,无需补发\n this._value = this._editor?.value ?? ''\n this._formAssociation.sync()\n this._autosizeEditor()\n }\n\n /**\n * blur 提交:与 Enter 同一条提交路径,草稿成为新值并派发 change。\n *\n * 不交还焦点:焦点已被用户移走,抢回宿主会吞掉消费者的目标焦点。\n */\n private _onBlur() {\n if (!this._editing || this._isDisabled) return\n this._commitEditing(false)\n }\n\n /**\n * 提交:草稿成为新值、退出编辑,并恰好派发一次 change。Enter 与 blur 同走这一条路。\n *\n * `returnFocus` 区分两种来路:Enter 主动结束时把焦点交还宿主(标记防重入编辑);\n * blur 是被动失焦,焦点已被用户移走,再抢回宿主会吞掉消费者的目标焦点。\n * 先退出编辑再动焦点:退出时 textarea 隐藏触发的 blur 看到 _editing 已为 false,\n * 不会把这次提交误判成又一次 blur 提交。\n */\n private _commitEditing(returnFocus: boolean) {\n if (!this._editing) return\n this._exitEditing()\n if (returnFocus) this._returnFocusToHost()\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n\n /**\n * 取消:恢复进入编辑时的值并派发 cancel。仅 Escape 走这一条路径。\n *\n * 调用方(Escape 分支)随后才交还焦点:本方法先摘除编辑态,焦点迁移触发的 blur\n * 到达时 `_editing` 已为 false 而早退,不会把取消误判成 blur 提交。\n */\n private _cancelEditing() {\n if (!this._editing) return\n this._value = this._editBase\n this._formAssociation.sync()\n this._exitEditing()\n /*\n * 重入守卫先于 dispatch 置位:cancel 同步派发,消费者可能在监听器里调\n * el.focus()——那是一次真实的宿主 focus,没有守卫就会重新进入编辑,随后一次\n * blur 便把恢复后的原值当成新草稿提交,用户什么都没改却收到 change。守卫在此\n * 置位后即被该次 focus 消费,与取消后由组件自己交还焦点走同一条早退路径。\n */\n this._refocusing = true\n /*\n * cancel 与原生 <dialog> 的 cancel 同名,而 dialog 的关闭管线就监听这个事件名。\n * 组件被消费方投映进浮层 shadow(drawer/dialog 标题)时,任何冒泡的 cancel——\n * composed 与否都一样,slot 都会把事件带进 shadow 树——都会被那个 dialog 当成\n * 一次关闭请求:issue #159 的实测症状是 drawer 标题聚焦编辑时按 Escape,或点到\n * 抽屉别处触发 blur,抽屉跟着编辑一起关。因此只在宿主上派发,不冒泡不组合;\n * 监听挂在组件本身即可(vue @cancel、React addEventListener 均不受影响)。\n * 手法同 checkbox/radio 受管时的事件策略(见各自 handleClick 注释)。\n */\n this.dispatchEvent(new Event('cancel', { bubbles: false, composed: false }))\n }\n\n /**\n * 焦点交还宿主:先标记再聚焦,宿主 focus 到达时标记已消费,不会再次进入编辑;\n * 标记在微任务里清掉,此后的聚焦照常进入编辑。\n */\n private _returnFocusToHost() {\n this._refocusing = true\n this.focus()\n queueMicrotask(() => {\n this._refocusing = false\n })\n }\n\n /**\n * 编辑态按键归属:Enter 提交、Escape 取消。\n *\n * 监听挂在 window 捕获阶段而不是 shadow 内的 textarea 上:消费方(浮层仲裁者)在\n * document 捕获阶段收 Escape,组件若只在 textarea 上处理,按键先被仲裁者收走,\n * 于是一次 Escape 会同时取消编辑和关闭外层浮层。window 在 document 上游,\n * stopPropagation 只有挂在这里才拦得住仲裁者。\n */\n private _claimEditingKeys() {\n window.addEventListener('keydown', this._onEditingKeydown, true)\n }\n\n private _releaseEditingKeys() {\n window.removeEventListener('keydown', this._onEditingKeydown, true)\n }\n\n private readonly _onEditingKeydown = (e: KeyboardEvent) => {\n // 输入法组合期间的 Enter/Escape 属于组合会话,交给 IME\n if (e.isComposing) return\n if (!e.composedPath().includes(this)) return\n if (e.key === 'Escape') {\n // 按键本身也被编辑层消费:外层浮层不应收到同一次 Escape\n e.preventDefault()\n e.stopPropagation()\n // 先取消(内部摘除编辑态)再交还焦点:focus 触发的 blur 看到 _editing 已为\n // false 而早退,不会把这次取消误判成 blur 提交\n this._cancelEditing()\n this._returnFocusToHost()\n return\n }\n if (e.key === 'Enter') {\n /*\n * 与 Escape 同套消费策略:preventDefault 压掉 textarea 的换行默认行为,\n * stopPropagation 让按键不外泄。取舍是明说的:编辑态 Enter 属于提交,外层\n * 表单的隐式提交与浮层监听不应收到同一次按键——正如 Escape 属于取消。\n * 此前只 preventDefault 不对称,Enter 会漏进外层监听。\n */\n e.preventDefault()\n e.stopPropagation()\n this._commitEditing(true)\n }\n }\n\n private _setupAutosize() {\n this._teardownAutosize()\n // jsdom 等无 ResizeObserver 的环境退化为按渲染 autosize,够用且不抛错\n if (typeof ResizeObserver === 'undefined') {\n this._autosizeEditor()\n return\n }\n this._resizeObserver = new ResizeObserver(() => this._autosizeEditor())\n this._resizeObserver.observe(this)\n this._autosizeEditor()\n }\n\n private _teardownAutosize() {\n this._resizeObserver?.disconnect()\n this._resizeObserver = null\n this._editor?.style.removeProperty('height')\n }\n\n /**\n * 编辑层高度始终跟随自身内容,不依赖文本层折出的行盒。\n *\n * 编辑层绝对定位并用 top/bottom 撑高,`height: auto` 量到的是包含块高度而不是内容高;\n * 先归零再读 scrollHeight,拿到的才是自身内容的高度(含 1px 内边距)。\n * 观察宿主而非编辑层:编辑层在文档流外,改它的高度不会反过来改动宿主尺寸。\n */\n private _autosizeEditor() {\n const editor = this._editor\n if (!editor) return\n const previous = editor.style.height\n editor.style.height = '0px'\n const height = editor.scrollHeight\n editor.style.height = height > 0 ? `${height}px` : previous\n }\n\n private get _displayText(): string {\n if (this._value !== '') return this._value\n if (this.placeholder !== '') return this.placeholder\n return BLANK_GLYPH\n }\n\n override render() {\n return html`\n <div class=\"layers\" @pointerdown=${this._onPointerDown} @click=${this._onClick}>\n <span class=${classMap({ text: true, placeholder: this._value === '' })}>${this._displayText}</span>\n <textarea\n class=\"editor\"\n .value=${this._value}\n .placeholder=${this.placeholder}\n ?disabled=${this._isDisabled}\n aria-label=${ifDefined(this.ariaLabel)}\n @input=${this._onInput}\n @blur=${this._onBlur}\n ></textarea>\n </div>\n `\n }\n\n declare readonly $events: {\n input: Event\n change: Event\n cancel: Event\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-editable-text': WebUiEditableText\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/editable-text/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\nimport { ifDefined } from 'lit/directives/if-defined.js'\n\nimport { FormAssociated, defineFormAssociation, FormAssociationController } from '@/shared/form-association'\nimport { createFieldId } from '@/shared/form-association/field-id'\n\nimport style from './style.css?inline'\n\n/** 空值且无 placeholder 时占位,保留一个行盒,避免宿主退化成 0 高度、无法点击进入编辑。 */\nconst BLANK_GLYPH = '\\u200B'\n\n/** 行/字符落点二分的容差(px),吸收次像素与舍入差异。 */\nconst CARET_TOLERANCE = 1\n\n/**\n * 取文本层承载文案的 Text 节点。\n *\n * 绑定表达式位于元素首个位置时,Lit 会在它前面留一个注释标记节点,因此不能用\n * `firstChild`:那会拿到没有布局的注释,Range 量不到任何矩形。\n */\nfunction textNodeOf(layer: HTMLElement): Text | null {\n for (const child of layer.childNodes) {\n if (child instanceof Text) return child\n }\n return null\n}\n\n/**\n * 把视口坐标映射为文本层的字符偏移。\n *\n * `document.caretPositionFromPoint` 不进入 shadow DOM(对 shadow 内容返回 shadow host\n * 之外的节点),因此在文本层上用 Range 空矩形二分:先按 y 选行,再在行内按 x 选字符。\n * 文本层与编辑层同盒同排版,偏移与 textarea 的 selectionStart 一一对应。\n */\nfunction caretOffsetFromPoint(textLayer: HTMLElement, x: number, y: number): number {\n const node = textNodeOf(textLayer)\n if (!node) return 0\n const length = node.data.length\n const range = document.createRange()\n const rectAt = (offset: number): DOMRect => {\n range.setStart(node, offset)\n range.setEnd(node, offset)\n return range.getBoundingClientRect()\n }\n\n // 同一行的盒可能被拆成多个 client rect;按 top 去重得到行列表,取与 y 最近的一行,\n // 距离相等时取更靠下的一行(与浏览器在行间的落点行为一致)。\n const lines: DOMRect[] = []\n for (const rect of textLayer.getClientRects()) {\n if (!lines.some(line => Math.abs(line.top - rect.top) <= CARET_TOLERANCE)) lines.push(rect)\n }\n if (lines.length === 0) return length\n let line = lines[0]\n let nearest = Infinity\n for (const candidate of lines) {\n const distance = Math.abs((candidate.top + candidate.bottom) / 2 - y)\n if (distance <= nearest) {\n nearest = distance\n line = candidate\n }\n }\n\n // 行首:caret top 首次落入本行的最小偏移\n let lo = 0\n let hi = length\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if (rectAt(mid).top >= line.top - CARET_TOLERANCE) hi = mid\n else lo = mid + 1\n }\n const lineStart = lo\n // 行尾:caret top 仍落在本行的最大偏移\n lo = lineStart\n hi = length\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1\n if (rectAt(mid).top <= line.top + CARET_TOLERANCE) lo = mid\n else hi = mid - 1\n }\n const lineEnd = Math.max(lo, lineStart)\n // 行内:caret left 不超过 x 的最大偏移\n lo = lineStart\n hi = lineEnd\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1\n if (rectAt(mid).left <= x + CARET_TOLERANCE) lo = mid\n else hi = mid - 1\n }\n return lo\n}\n\n@customElement('web-ui-editable-text')\nexport class WebUiEditableText extends FormAssociated(LitElement) {\n static override styles = [unsafeCSS(style)]\n\n @property({ type: String, reflect: true }) name = ''\n @property({ type: String, reflect: true }) placeholder = ''\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true }) readonly = false\n\n /**\n * 卸载时按提交语义结束编辑,默认 false。\n *\n * 跨引擎实测(Chromium 与 WebKit 各一轮)结论**不一致**,这是本属性的主要价值来源:\n *\n * - Chromium:移除正在编辑的组件会向编辑层派发 `blur`,默认行为下 `change` 已照常派发\n * 一次,草稿不丢。blur 与 change 同步排在 `disconnectedCallback` 之后(后者在\n * disconnected 步骤内跑完),卸载因此没拿到一条「排在该消费方状态更新之前」的提交来源。\n * - WebKit(Safari / iOS):移除**不派发 blur**,默认路径什么都不提交,草稿随组件一起丢弃。\n * 这里 blur 兜底不存在,只有显式开启本属性才能保住草稿。\n * - Gecko(Firefox)**未验证**:实测所用机器上没有任何 Firefox 能启动,这里不做断言。把它当作\n * 未知,而不是默认与上面两者之一相同。\n *\n * 所以在 Safari 上这不是可选优化而是必需的:默认值 `false` 对 Safari 用户等于没有兜底,\n * 消费方若依赖「行被回收时草稿不丢」,在 Safari 上必须显式开启本属性。\n *\n * 开启后由 `disconnectedCallback` 复用 `_commitEditing`,与 `Enter`/`blur` 是同一份实现,\n * 卸载不是第三条独立的提交语义。副作用与 `blur` 提交一致:草稿成为新值、退出编辑、\n * 派发一次 `change`(冒泡且组合)。两处与 `blur` 刻意不同:\n *\n * 1. 提交判据——`Enter` 与 `blur` 由用户动作触发,一律算提交意图;卸载没有这种意图,\n * 因此只在草稿与进入编辑时的值确实不同时才提交,不产生空提交。\n * 2. 禁用与只读态——`disabled` / `readonly` 时与 `_onBlur` 一致:不派发提交事件。两者对「已有\n * 草稿」的后果不同,必须分开说:`disabled` 下草稿保留在组件上;`readonly` 下草稿是真的\n * 丢掉——`readonly` 阻止不了先前的输入,而卸载时不会再有 blur 兜底(WebKit 上连 blur\n * 都没有)。可达路径是「可编辑态进入 → 起草稿 → 期间翻 readonly → 卸载」。这里不按取消\n * 处理:cancel 的契约是恢复 `_editBase`,而卸载既没有焦点要交还也没有宿主接收结果,\n * 改成 cancel 还会与「与 `_onBlur` 一致」这条公开承诺相悖。用户看到的是「我打的字没了」,\n * 既没有 change 也没有 cancel。\n *\n * 派发发生在卸载回调内,`change` 沿组合路径向上派发,因此挂在组件自身或任一祖先上的监听\n * 都会执行,**包括已经随宿主一起被卸载的祖先**——派发不要求祖先仍在文档中。这正是虚拟化\n * 场景需要的:消费方的监听器往往就挂在被回收的那一行上。因此监听器不能假定组件此刻仍在\n * 文档里;需要把结果交给宿主树之外的状态时,用 `queueMicrotask` 之类的显式转交。\n *\n * 祖先上的 `display: none` 不是卸载:组件仍在文档中、不派发任何事件(两引擎一致),那种\n * 场合需要消费方主动结束编辑。\n */\n @property({ type: Boolean, reflect: true, attribute: 'commit-on-unmount' }) commitOnUnmount = false\n\n @property({ type: String, attribute: 'aria-label' }) override ariaLabel: string | null = null\n\n @state() private _value = ''\n\n /**\n * `value` attribute 承载 form reset 的初值,不随当前值反射;property 暴露当前值,\n * 与原生 input 的 value / defaultValue 关系一致。\n */\n @property({ type: String })\n get value(): string {\n return this._value\n }\n\n set value(v: string) {\n const old = this._value\n this._value = v\n this._formAssociation.sync()\n this.requestUpdate('value', old)\n }\n\n @query('.editor') private _editor?: HTMLTextAreaElement\n @query('.text') private _textLayer?: HTMLElement\n\n private _editing = false\n private _editBase = ''\n private _pendingCaret: number | null = null\n private _refocusing = false\n private _resizeObserver: ResizeObserver | null = null\n private readonly _fieldId = createFieldId('web-ui-editable-text')\n\n private get _isDisabled(): boolean {\n return this.disabled || this._formAssociation.isFormDisabled()\n }\n\n private readonly _formAssociation = defineFormAssociation<string>({\n host: this,\n getState: () => this._value,\n setState: value => {\n this.value = value\n },\n getFormValue: () => this._value,\n getFormState: () => this._value,\n restoreState: state => {\n if (typeof state === 'string') this.value = state\n }\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n constructor() {\n super()\n // focus 不冒泡:宿主监听只在焦点从组件外进入时触发,shadow 内聚焦不会误触\n this.addEventListener('focus', this._onHostFocus)\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n /*\n * 卸载没有用户动作作为提交意图(`Enter` / `blur` 都有),所以判据是草稿与进入编辑时的\n * 值是否不同,而不是「是否提交过」——Escape 会把值恢复成 `_editBase`,但那是取消,与\n * 本次卸载无关;一个只聚焦了没打字的会话也不该在消失时凭空产生一次 change。\n *\n * `_isDisabled` 与 `readonly` 两道守卫都与 `_onBlur` 对称:两者都不派发提交事件。`disabled`\n * 必须显式判——`disabled = true` 与 `remove()` 在同一任务里执行时 Lit 更新是微任务,\n * `updated()` 还没跑到(禁用态退出编辑那条分支尚未执行),此时组件仍在编辑态,不判就会\n * 派发一条 blur 路径明确拒绝的 change。`readonly` 不能靠「草稿恒等于基线」兜住:可编辑态\n * 进入并起草稿之后属性才翻成 readonly 的会话,基线仍是进入编辑时的旧值,两条判据都成立\n * 却不该提交(草稿在这次卸载中丢失,理由见 property 的 JSDoc)。\n */\n if (\n this.commitOnUnmount &&\n this._editing &&\n !this._isDisabled &&\n !this.readonly &&\n this._value !== this._editBase\n ) {\n this._commitEditing(false)\n }\n this._releaseEditingKeys()\n this._teardownAutosize()\n }\n\n override connectedCallback() {\n super.connectedCallback()\n /*\n * autosize 的 ResizeObserver 在 disconnectedCallback 被拆掉,搭建若只放在\n * firstUpdated(一生一次),元素断开再重连后 RO 永久丢失、autosize 静默失效。\n * connectedCallback 每次连接都跑:_setupAutosize 自带 teardown,重复进入无副作用。\n * 首连接时渲染尚未发生(_editor 为 null),_autosizeEditor 与 RO 回调都有判空守卫。\n */\n this._setupAutosize()\n // 同类缺口:编辑态被断开时 _releaseEditingKeys 撤了 window 监听,重连后 _editing\n // 仍为 true(blur 取消未必先于断开到达),此时补领,避免重连后按键失灵。\n if (this._editing) this._claimEditingKeys()\n }\n\n override updated() {\n // disabled 时移出 tab 序列;编辑态由 editing attribute 表达,二者互斥\n if (this._isDisabled) this.removeAttribute('tabindex')\n else this.setAttribute('tabindex', '0')\n // 禁用态生效时(属性或 fieldset)退出编辑:值保持当前草稿,不派发提交事件\n if (this._editing && this._isDisabled) this._exitEditing()\n this._autosizeEditor()\n }\n\n private _onHostFocus() {\n if (this._refocusing) {\n this._refocusing = false\n return\n }\n this._enterEditing()\n }\n\n /**\n * pointerdown 时文本层仍是命中目标,此时记录落点;浏览器随后的默认聚焦会触发\n * host focus 进入编辑,偏移在那里应用到 textarea。\n */\n private _onPointerDown(e: PointerEvent) {\n if (this._isDisabled || this._editing || !this._textLayer) return\n this._pendingCaret = caretOffsetFromPoint(this._textLayer, e.clientX, e.clientY)\n }\n\n /** mousedown 默认聚焦被拦截等场景由 click 兜底进入编辑。 */\n private _onClick() {\n if (this._isDisabled || this._editing) return\n this._enterEditing()\n }\n\n /**\n * 公共 API:进入编辑态并全选内容;已在编辑态时只重新全选。\n *\n * `disabled` 时不进入编辑,与 `focus()` 一致。\n */\n select() {\n if (this._isDisabled) return\n if (!this._editing) this._enterEditing()\n const editor = this._editor\n // 全选:末端显式取当前文本长度,覆盖 _enterEditing 落下的光标位置\n editor?.setSelectionRange(0, editor.value.length)\n }\n\n private _enterEditing() {\n if (this._isDisabled || this._editing) return\n this._editing = true\n this._editBase = this._value\n this.toggleAttribute('editing', true)\n this._claimEditingKeys()\n const editor = this._editor\n if (!editor) return\n editor.focus()\n // Tab / 编程式聚焦光标到末尾;指针落点用 pointerdown 记下的偏移\n const offset = this._pendingCaret ?? this._value.length\n this._pendingCaret = null\n const clamped = Math.max(0, Math.min(offset, editor.value.length))\n editor.setSelectionRange(clamped, clamped)\n }\n\n private _exitEditing() {\n if (!this._editing) return\n this._editing = false\n this._releaseEditingKeys()\n this.toggleAttribute('editing', false)\n }\n\n private _onInput() {\n // 原生 input 已 composed 冒泡出 shadow root 且 target 重定向到宿主,无需补发\n const editor = this._editor\n if (this._isDisabled || this.readonly) {\n if (editor && editor.value !== this._value) editor.value = this._value\n return\n }\n this._value = editor?.value ?? ''\n this._formAssociation.sync()\n this._autosizeEditor()\n }\n\n /**\n * blur 提交:与 Enter 同一条提交路径,草稿成为新值并派发 change。\n *\n * 不交还焦点:焦点已被用户移走,抢回宿主会吞掉消费者的目标焦点。\n */\n private _onBlur() {\n if (!this._editing || this._isDisabled) return\n if (this.readonly) {\n this._exitEditing()\n return\n }\n this._commitEditing(false)\n }\n\n /**\n * 提交:草稿成为新值、退出编辑,并恰好派发一次 change。Enter 与 blur 同走这一条路。\n *\n * `returnFocus` 区分两种来路:Enter 主动结束时把焦点交还宿主(标记防重入编辑);\n * blur 是被动失焦,焦点已被用户移走,再抢回宿主会吞掉消费者的目标焦点。\n * 先退出编辑再动焦点:退出时 textarea 隐藏触发的 blur 看到 _editing 已为 false,\n * 不会把这次提交误判成又一次 blur 提交。\n */\n private _commitEditing(returnFocus: boolean) {\n if (!this._editing) return\n this._exitEditing()\n if (returnFocus) this._returnFocusToHost()\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n\n /**\n * 取消:恢复进入编辑时的值并派发 cancel。仅 Escape 走这一条路径。\n *\n * 调用方(Escape 分支)随后才交还焦点:本方法先摘除编辑态,焦点迁移触发的 blur\n * 到达时 `_editing` 已为 false 而早退,不会把取消误判成 blur 提交。\n */\n private _cancelEditing() {\n if (!this._editing) return\n this._value = this._editBase\n this._formAssociation.sync()\n this._exitEditing()\n /*\n * 重入守卫先于 dispatch 置位:cancel 同步派发,消费者可能在监听器里调\n * el.focus()——那是一次真实的宿主 focus,没有守卫就会重新进入编辑,随后一次\n * blur 便把恢复后的原值当成新草稿提交,用户什么都没改却收到 change。守卫在此\n * 置位后即被该次 focus 消费,与取消后由组件自己交还焦点走同一条早退路径。\n */\n this._refocusing = true\n /*\n * cancel 与原生 <dialog> 的 cancel 同名,而 dialog 的关闭管线就监听这个事件名。\n * 组件被消费方投映进浮层 shadow(drawer/dialog 标题)时,任何冒泡的 cancel——\n * composed 与否都一样,slot 都会把事件带进 shadow 树——都会被那个 dialog 当成\n * 一次关闭请求:issue #159 的实测症状是 drawer 标题聚焦编辑时按 Escape,或点到\n * 抽屉别处触发 blur,抽屉跟着编辑一起关。因此只在宿主上派发,不冒泡不组合;\n * 监听挂在组件本身即可(vue @cancel、React addEventListener 均不受影响)。\n * 手法同 checkbox/radio 受管时的事件策略(见各自 handleClick 注释)。\n */\n this.dispatchEvent(new Event('cancel', { bubbles: false, composed: false }))\n }\n\n /**\n * 焦点交还宿主:先标记再聚焦,宿主 focus 到达时标记已消费,不会再次进入编辑;\n * 标记在微任务里清掉,此后的聚焦照常进入编辑。\n */\n private _returnFocusToHost() {\n this._refocusing = true\n this.focus()\n queueMicrotask(() => {\n this._refocusing = false\n })\n }\n\n /**\n * 编辑态按键归属:Enter 提交、Escape 取消。\n *\n * 监听挂在 window 捕获阶段而不是 shadow 内的 textarea 上:消费方(浮层仲裁者)在\n * document 捕获阶段收 Escape,组件若只在 textarea 上处理,按键先被仲裁者收走,\n * 于是一次 Escape 会同时取消编辑和关闭外层浮层。window 在 document 上游,\n * stopPropagation 只有挂在这里才拦得住仲裁者。\n */\n private _claimEditingKeys() {\n window.addEventListener('keydown', this._onEditingKeydown, true)\n }\n\n private _releaseEditingKeys() {\n window.removeEventListener('keydown', this._onEditingKeydown, true)\n }\n\n private readonly _onEditingKeydown = (e: KeyboardEvent) => {\n // 输入法组合期间的 Enter/Escape 属于组合会话,交给 IME\n if (e.isComposing) return\n if (!e.composedPath().includes(this)) return\n if (e.key === 'Escape') {\n // 按键本身也被编辑层消费:外层浮层不应收到同一次 Escape\n e.preventDefault()\n e.stopPropagation()\n // 先取消(内部摘除编辑态)再交还焦点:focus 触发的 blur 看到 _editing 已为\n // false 而早退,不会把这次取消误判成 blur 提交\n this._cancelEditing()\n this._returnFocusToHost()\n return\n }\n if (e.key === 'Enter') {\n /*\n * Enter 无论是否只读都属于编辑层,消费策略与 Escape 对称:preventDefault\n * 压掉默认行为,stopPropagation 阻止外层表单隐式提交或浮层监听收到按键。\n * 只读态只是没有草稿可提交,因此消费后直接返回,不退出编辑、不派发 change。\n */\n e.preventDefault()\n e.stopPropagation()\n if (this.readonly) return\n /*\n * 可编辑态的 Enter 随后提交草稿;外层表单的隐式提交与浮层监听不应收到\n * 同一次按键——正如 Escape 属于取消。\n */\n this._commitEditing(true)\n }\n }\n\n private _setupAutosize() {\n this._teardownAutosize()\n // jsdom 等无 ResizeObserver 的环境退化为按渲染 autosize,够用且不抛错\n if (typeof ResizeObserver === 'undefined') {\n this._autosizeEditor()\n return\n }\n this._resizeObserver = new ResizeObserver(() => this._autosizeEditor())\n this._resizeObserver.observe(this)\n this._autosizeEditor()\n }\n\n private _teardownAutosize() {\n this._resizeObserver?.disconnect()\n this._resizeObserver = null\n this._editor?.style.removeProperty('height')\n }\n\n /**\n * 编辑层高度始终跟随自身内容,不依赖文本层折出的行盒。\n *\n * 编辑层绝对定位并用 top/bottom 撑高,`height: auto` 量到的是包含块高度而不是内容高;\n * 先归零再读 scrollHeight,拿到的才是自身内容的高度(含 1px 内边距)。\n * 观察宿主而非编辑层:编辑层在文档流外,改它的高度不会反过来改动宿主尺寸。\n */\n private _autosizeEditor() {\n const editor = this._editor\n if (!editor) return\n const previous = editor.style.height\n editor.style.height = '0px'\n const height = editor.scrollHeight\n editor.style.height = height > 0 ? `${height}px` : previous\n }\n\n private get _displayText(): string {\n if (this._value !== '') return this._value\n if (this.placeholder !== '') return this.placeholder\n return BLANK_GLYPH\n }\n\n override render() {\n return html`\n <div class=\"layers\" @pointerdown=${this._onPointerDown} @click=${this._onClick}>\n <span class=${classMap({ text: true, placeholder: this._value === '' })}>${this._displayText}</span>\n <textarea\n id=${this._fieldId}\n class=\"editor\"\n .value=${this._value}\n .placeholder=${this.placeholder}\n ?disabled=${this._isDisabled}\n ?readonly=${this.readonly}\n aria-label=${ifDefined(this.ariaLabel)}\n @input=${this._onInput}\n @blur=${this._onBlur}\n ></textarea>\n </div>\n `\n }\n\n declare readonly $events: {\n input: Event\n change: Event\n cancel: Event\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-editable-text': WebUiEditableText\n }\n}\n"],"mappings":";;;;;;;;;AAWA,IAAM,IAAc,KAGd,IAAkB;AAQxB,SAAS,EAAW,GAAiC;CACnD,KAAK,IAAM,KAAS,EAAM,YACxB,IAAI,aAAiB,MAAM,OAAO;CAEpC,OAAO;AACT;AASA,SAAS,EAAqB,GAAwB,GAAW,GAAmB;CAClF,IAAM,IAAO,EAAW,CAAS;CACjC,IAAI,CAAC,GAAM,OAAO;CAClB,IAAM,IAAS,EAAK,KAAK,QACnB,IAAQ,SAAS,YAAY,GAC7B,KAAU,OACd,EAAM,SAAS,GAAM,CAAM,GAC3B,EAAM,OAAO,GAAM,CAAM,GAClB,EAAM,sBAAsB,IAK/B,IAAmB,CAAC;CAC1B,KAAK,IAAM,KAAQ,EAAU,eAAe,GAC1C,AAAK,EAAM,MAAK,MAAQ,KAAK,IAAI,EAAK,MAAM,EAAK,GAAG,KAAK,CAAe,KAAG,EAAM,KAAK,CAAI;CAE5F,IAAI,EAAM,WAAW,GAAG,OAAO;CAC/B,IAAI,IAAO,EAAM,IACb,IAAU;CACd,KAAK,IAAM,KAAa,GAAO;EAC7B,IAAM,IAAW,KAAK,KAAK,EAAU,MAAM,EAAU,UAAU,IAAI,CAAC;EACpE,AAAI,KAAY,MACd,IAAU,GACV,IAAO;CAEX;CAGA,IAAI,IAAK,GACL,IAAK;CACT,OAAO,IAAK,IAAI;EACd,IAAM,IAAO,IAAK,KAAO;EACzB,AAAI,EAAO,CAAG,CAAC,CAAC,OAAO,EAAK,MAAM,IAAiB,IAAK,IACnD,IAAK,IAAM;CAClB;CACA,IAAM,IAAY;CAIlB,KAFA,IAAK,GACL,IAAK,GACE,IAAK,IAAI;EACd,IAAM,IAAO,IAAK,IAAK,KAAM;EAC7B,AAAI,EAAO,CAAG,CAAC,CAAC,OAAO,EAAK,MAAM,IAAiB,IAAK,IACnD,IAAK,IAAM;CAClB;CACA,IAAM,IAAU,KAAK,IAAI,GAAI,CAAS;CAItC,KAFA,IAAK,GACL,IAAK,GACE,IAAK,IAAI;EACd,IAAM,IAAO,IAAK,IAAK,KAAM;EAC7B,AAAI,EAAO,CAAG,CAAC,CAAC,QAAQ,IAAI,IAAiB,IAAK,IAC7C,IAAK,IAAM;CAClB;CACA,OAAO;AACT;AAGO,IAAM,IAAN,cAAgC,EAAe,CAAU,CAAC,CAAC;;EACvC,KAAA,SAAA,CAAC,EAAU,CAAK,CAAC;;CAuD1C,IACI,QAAgB;EAClB,OAAO,KAAK;CACd;CAEA,IAAI,MAAM,GAAW;EACnB,IAAM,IAAM,KAAK;EAGjB,AAFA,KAAK,SAAS,GACd,KAAK,iBAAiB,KAAK,GAC3B,KAAK,cAAc,SAAS,CAAG;CACjC;CAYA,IAAY,cAAuB;EACjC,OAAO,KAAK,YAAY,KAAK,iBAAiB,eAAe;CAC/D;CAiBA,cAAc;EAGZ,AAFA,MAAM,GA/F0C,KAAA,OAAA,IACO,KAAA,cAAA,IACF,KAAA,WAAA,IACA,KAAA,WAAA,IAwCuC,KAAA,kBAAA,IAEL,KAAA,YAAA,MAE/D,KAAA,SAAA,IAqBP,KAAA,WAAA,IACC,KAAA,YAAA,IACmB,KAAA,gBAAA,MACjB,KAAA,cAAA,IAC2B,KAAA,kBAAA,MACrB,KAAA,WAAA,EAAc,sBAAsB,GAM5B,KAAA,mBAAA,EAA8B;GAChE,MAAM;GACN,gBAAgB,KAAK;GACrB,WAAU,MAAS;IACjB,KAAK,QAAQ;GACf;GACA,oBAAoB,KAAK;GACzB,oBAAoB,KAAK;GACzB,eAAc,MAAS;IACrB,AAAI,OAAO,KAAU,aAAU,KAAK,QAAQ;GAC9C;EACF,CAAC,CAAC,CAAC,KAAK,GAEsC,KAAA,6BAAA,IAAI,EAA0B,MAAM,KAAK,gBAAgB,GAwNjE,KAAA,qBAAA,MAAqB;GAErD,OAAE,eACD,EAAE,aAAa,CAAC,CAAC,SAAS,IAAI,GACnC;QAAI,EAAE,QAAQ,UAAU;KAOtB,AALA,EAAE,eAAe,GACjB,EAAE,gBAAgB,GAGlB,KAAK,eAAe,GACpB,KAAK,mBAAmB;KACxB;IACF;IACA,IAAI,EAAE,QAAQ,SAAS;KAQrB,IAFA,EAAE,eAAe,GACjB,EAAE,gBAAgB,GACd,KAAK,UAAU;KAKnB,KAAK,eAAe,EAAI;IAC1B;GAfA;EAgBF,GAhPE,KAAK,iBAAiB,SAAS,KAAK,YAAY;CAClD;CAEA,uBAAgC;EAwB9B,AAvBA,MAAM,qBAAqB,GAczB,KAAK,mBACL,KAAK,YACL,CAAC,KAAK,eACN,CAAC,KAAK,YACN,KAAK,WAAW,KAAK,aAErB,KAAK,eAAe,EAAK,GAE3B,KAAK,oBAAoB,GACzB,KAAK,kBAAkB;CACzB;CAEA,oBAA6B;EAW3B,AAVA,MAAM,kBAAkB,GAOxB,KAAK,eAAe,GAGhB,KAAK,YAAU,KAAK,kBAAkB;CAC5C;CAEA,UAAmB;EAMjB,AAJI,KAAK,cAAa,KAAK,gBAAgB,UAAU,IAChD,KAAK,aAAa,YAAY,GAAG,GAElC,KAAK,YAAY,KAAK,eAAa,KAAK,aAAa,GACzD,KAAK,gBAAgB;CACvB;CAEA,eAAuB;EACrB,IAAI,KAAK,aAAa;GACpB,KAAK,cAAc;GACnB;EACF;EACA,KAAK,cAAc;CACrB;CAMA,eAAuB,GAAiB;EAClC,KAAK,eAAe,KAAK,YAAY,CAAC,KAAK,eAC/C,KAAK,gBAAgB,EAAqB,KAAK,YAAY,EAAE,SAAS,EAAE,OAAO;CACjF;CAGA,WAAmB;EACb,KAAK,eAAe,KAAK,YAC7B,KAAK,cAAc;CACrB;CAOA,SAAS;EACP,IAAI,KAAK,aAAa;EACtB,AAAK,KAAK,YAAU,KAAK,cAAc;EACvC,IAAM,IAAS,KAAK;EAEpB,GAAQ,kBAAkB,GAAG,EAAO,MAAM,MAAM;CAClD;CAEA,gBAAwB;EACtB,IAAI,KAAK,eAAe,KAAK,UAAU;EAIvC,AAHA,KAAK,WAAW,IAChB,KAAK,YAAY,KAAK,QACtB,KAAK,gBAAgB,WAAW,EAAI,GACpC,KAAK,kBAAkB;EACvB,IAAM,IAAS,KAAK;EACpB,IAAI,CAAC,GAAQ;EACb,EAAO,MAAM;EAEb,IAAM,IAAS,KAAK,iBAAiB,KAAK,OAAO;EACjD,KAAK,gBAAgB;EACrB,IAAM,IAAU,KAAK,IAAI,GAAG,KAAK,IAAI,GAAQ,EAAO,MAAM,MAAM,CAAC;EACjE,EAAO,kBAAkB,GAAS,CAAO;CAC3C;CAEA,eAAuB;EAChB,KAAK,aACV,KAAK,WAAW,IAChB,KAAK,oBAAoB,GACzB,KAAK,gBAAgB,WAAW,EAAK;CACvC;CAEA,WAAmB;EAEjB,IAAM,IAAS,KAAK;EACpB,IAAI,KAAK,eAAe,KAAK,UAAU;GACrC,AAAI,KAAU,EAAO,UAAU,KAAK,WAAQ,EAAO,QAAQ,KAAK;GAChE;EACF;EAGA,AAFA,KAAK,SAAS,GAAQ,SAAS,IAC/B,KAAK,iBAAiB,KAAK,GAC3B,KAAK,gBAAgB;CACvB;CAOA,UAAkB;EACZ,IAAC,KAAK,YAAY,MAAK,aAC3B;OAAI,KAAK,UAAU;IACjB,KAAK,aAAa;IAClB;GACF;GACA,KAAK,eAAe,EAAK;EADzB;CAEF;CAUA,eAAuB,GAAsB;EACtC,KAAK,aACV,KAAK,aAAa,GACd,KAAa,KAAK,mBAAmB,GACzC,KAAK,cAAc,IAAI,MAAM,UAAU;GAAE,SAAS;GAAM,UAAU;EAAK,CAAC,CAAC;CAC3E;CAQA,iBAAyB;EAClB,KAAK,aACV,KAAK,SAAS,KAAK,WACnB,KAAK,iBAAiB,KAAK,GAC3B,KAAK,aAAa,GAOlB,KAAK,cAAc,IAUnB,KAAK,cAAc,IAAI,MAAM,UAAU;GAAE,SAAS;GAAO,UAAU;EAAM,CAAC,CAAC;CAC7E;CAMA,qBAA6B;EAG3B,AAFA,KAAK,cAAc,IACnB,KAAK,MAAM,GACX,qBAAqB;GACnB,KAAK,cAAc;EACrB,CAAC;CACH;CAUA,oBAA4B;EAC1B,OAAO,iBAAiB,WAAW,KAAK,mBAAmB,EAAI;CACjE;CAEA,sBAA8B;EAC5B,OAAO,oBAAoB,WAAW,KAAK,mBAAmB,EAAI;CACpE;CAiCA,iBAAyB;EAGvB,IAFA,KAAK,kBAAkB,GAEnB,OAAO,iBAAmB,KAAa;GACzC,KAAK,gBAAgB;GACrB;EACF;EAGA,AAFA,KAAK,kBAAkB,IAAI,qBAAqB,KAAK,gBAAgB,CAAC,GACtE,KAAK,gBAAgB,QAAQ,IAAI,GACjC,KAAK,gBAAgB;CACvB;CAEA,oBAA4B;EAG1B,AAFA,KAAK,iBAAiB,WAAW,GACjC,KAAK,kBAAkB,MACvB,KAAK,SAAS,MAAM,eAAe,QAAQ;CAC7C;CASA,kBAA0B;EACxB,IAAM,IAAS,KAAK;EACpB,IAAI,CAAC,GAAQ;EACb,IAAM,IAAW,EAAO,MAAM;EAC9B,EAAO,MAAM,SAAS;EACtB,IAAM,IAAS,EAAO;EACtB,EAAO,MAAM,SAAS,IAAS,IAAI,GAAG,EAAO,MAAM;CACrD;CAEA,IAAY,eAAuB;EAGjC,OAFI,KAAK,WAAW,KAChB,KAAK,gBAAgB,KAClB,IAD6B,KAAK,cADV,KAAK;CAGtC;CAEA,SAAkB;EAChB,OAAO,CAAI;yCAC0B,KAAK,eAAe,UAAU,KAAK,SAAS;sBAC/D,EAAS;GAAE,MAAM;GAAM,aAAa,KAAK,WAAW;EAAG,CAAC,EAAE,GAAG,KAAK,aAAa;;eAEtF,KAAK,SAAS;;mBAEV,KAAK,OAAO;yBACN,KAAK,YAAY;sBACpB,KAAK,YAAY;sBACjB,KAAK,SAAS;uBACb,EAAU,KAAK,SAAS,EAAE;mBAC9B,KAAK,SAAS;kBACf,KAAK,QAAQ;;;;CAI7B;AAOF;AAnZG,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,eAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GAwCzC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;CAAM,WAAW;AAAoB,CAAC,CAAA,GAAA,EAAA,WAAA,mBAAA,KAAA,CAAA,GAEzE,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,WAAW;AAAa,CAAC,CAAA,GAAA,EAAA,WAAA,aAAA,KAAA,CAAA,GAElD,EAAA,CAAA,EAAM,CAAA,GAAA,EAAA,WAAA,UAAA,KAAA,CAAA,GAMN,EAAA,CAAA,EAAS,EAAE,MAAM,OAAO,CAAC,CAAA,GAAA,EAAA,WAAA,SAAA,IAAA,GAYzB,EAAA,CAAA,EAAM,SAAS,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GACf,EAAA,CAAA,EAAM,OAAO,CAAA,GAAA,EAAA,WAAA,cAAA,KAAA,CAAA,GAtEf,IAAA,EAAA,CAAA,EAAc,sBAAsB,CAAA,GAAA,CAAA"}