@greypan/web-ui 9.1.0 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.CN.md +67 -28
- package/README.md +69 -28
- package/dist/components/autocomplete/index.js.map +1 -1
- package/dist/components/avatar/index.js +5 -5
- package/dist/components/avatar/index.js.map +1 -1
- package/dist/components/back-top/index.js.map +1 -1
- package/dist/components/badge/index.js +5 -5
- package/dist/components/button/index.js +5 -5
- package/dist/components/button/index.js.map +1 -1
- package/dist/components/checkbox/index.js.map +1 -1
- package/dist/components/context-menu/index.js +65 -65
- package/dist/components/dialog/index.d.ts +29 -4
- package/dist/components/dialog/index.js +26 -13
- package/dist/components/dialog/index.js.map +1 -1
- package/dist/components/dialog/style.js +1 -1
- package/dist/components/dialog/style.js.map +1 -1
- package/dist/components/drawer/index.js.map +1 -1
- package/dist/components/dropdown-item/index.js.map +1 -1
- package/dist/components/editable-text/index.js +4 -4
- package/dist/components/empty/index.js.map +1 -1
- package/dist/components/image-preview/index.js.map +1 -1
- package/dist/components/input/index.js +5 -5
- package/dist/components/input/index.js.map +1 -1
- package/dist/components/input-number/index.js.map +1 -1
- package/dist/components/layout/index.js.map +1 -1
- package/dist/components/option/index.js.map +1 -1
- package/dist/components/select/index.js.map +1 -1
- package/dist/components/switch/index.js.map +1 -1
- package/dist/components/textarea/index.js +8 -8
- package/dist/components/textarea/index.js.map +1 -1
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/scrollbars/index.js.map +1 -1
- package/dist/shared/overlay/overlay.js +33 -33
- package/dist/vite/index.d.ts +28 -0
- package/dist/vite/index.js +16 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +5 -1
- package/dist/node_modules/.pnpm/@floating-ui_core@1.8.0/node_modules/@floating-ui/core/dist/floating-ui.core.js +0 -246
- package/dist/node_modules/.pnpm/@floating-ui_core@1.8.0/node_modules/@floating-ui/core/dist/floating-ui.core.js.map +0 -1
- package/dist/node_modules/.pnpm/@floating-ui_dom@1.8.0/node_modules/@floating-ui/dom/dist/floating-ui.dom.js +0 -324
- package/dist/node_modules/.pnpm/@floating-ui_dom@1.8.0/node_modules/@floating-ui/dom/dist/floating-ui.dom.js.map +0 -1
- package/dist/node_modules/.pnpm/@floating-ui_utils@0.2.12/node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.js +0 -100
- package/dist/node_modules/.pnpm/@floating-ui_utils@0.2.12/node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.js.map +0 -1
- package/dist/node_modules/.pnpm/@floating-ui_utils@0.2.12/node_modules/@floating-ui/utils/dist/floating-ui.utils.js +0 -102
- package/dist/node_modules/.pnpm/@floating-ui_utils@0.2.12/node_modules/@floating-ui/utils/dist/floating-ui.utils.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/option/index.ts"],"sourcesContent":["import { html, LitElement, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/option/index.ts"],"sourcesContent":["import '@/components/icon'\n\nimport { html, LitElement, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { heroiconsCheck16Solid } from '@/icons'\n\nimport style from './style.css?inline'\n\n@customElement('web-ui-option')\nexport class WebUiOption extends LitElement {\n static override styles = unsafeCSS(style)\n\n @property({ type: String, reflect: true }) value = ''\n @property({ type: String })\n get label(): string {\n return this._label || this._getSlottedLabel()\n }\n set label(value: string) {\n const old = this.label\n this._label = value\n this.requestUpdate('label', old)\n }\n private _label = ''\n private _slottedLabel = ''\n\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true }) selected = false\n\n override connectedCallback() {\n super.connectedCallback()\n this.dispatchEvent(\n new CustomEvent('option-register', {\n bubbles: true,\n composed: true,\n detail: { value: this.value, label: this.label, disabled: this.disabled }\n })\n )\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this.dispatchEvent(\n new CustomEvent('option-unregister', {\n bubbles: true,\n composed: true,\n detail: { value: this.value }\n })\n )\n }\n\n override updated(changed: PropertyValues) {\n if (changed.has('value') || changed.has('label') || changed.has('disabled')) {\n this.dispatchEvent(\n new CustomEvent('option-update', {\n bubbles: true,\n composed: true\n })\n )\n }\n }\n\n private _onLabelSlotChange = () => {\n const label = this._getSlottedLabel()\n if (label === this._slottedLabel) return\n\n const old = this._label || this._slottedLabel\n this._slottedLabel = label\n this.requestUpdate('label', old)\n }\n\n private _getSlottedLabel(): string {\n return [...this.childNodes]\n .filter(node => !(node instanceof HTMLElement && node.slot))\n .map(node => node.textContent)\n .join('')\n .trim()\n }\n\n override render() {\n return html`\n <div class=\"option-label\">\n <web-ui-icon class=\"check\" size=\"16\" .icon=${heroiconsCheck16Solid}></web-ui-icon>\n <slot name=\"prefix\"></slot>\n <span class=\"content-wrap\">${this._label || html`<slot @slotchange=${this._onLabelSlotChange}></slot>`}</span>\n <slot name=\"suffix\"></slot>\n </div>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-option': WebUiOption\n }\n}\n"],"mappings":";;;;;;;AAUO,IAAM,IAAN,cAA0B,EAAW;;EAoDP,aAjDgB,KAAA,QAAA,IAUlC,KAAA,SAAA,IACO,KAAA,gBAAA,IAE+B,KAAA,WAAA,IACA,KAAA,WAAA,IAmCpB,KAAA,2BAAA;GACjC,IAAM,IAAQ,KAAK,iBAAiB;GACpC,IAAI,MAAU,KAAK,eAAe;GAElC,IAAM,IAAM,KAAK,UAAU,KAAK;GAEhC,AADA,KAAK,gBAAgB,GACrB,KAAK,cAAc,SAAS,CAAG;EACjC;;;EA1DyB,KAAA,SAAA,EAAU,CAAK;;CAGxC,IACI,QAAgB;EAClB,OAAO,KAAK,UAAU,KAAK,iBAAiB;CAC9C;CACA,IAAI,MAAM,GAAe;EACvB,IAAM,IAAM,KAAK;EAEjB,AADA,KAAK,SAAS,GACd,KAAK,cAAc,SAAS,CAAG;CACjC;CAOA,oBAA6B;EAE3B,AADA,MAAM,kBAAkB,GACxB,KAAK,cACH,IAAI,YAAY,mBAAmB;GACjC,SAAS;GACT,UAAU;GACV,QAAQ;IAAE,OAAO,KAAK;IAAO,OAAO,KAAK;IAAO,UAAU,KAAK;GAAS;EAC1E,CAAC,CACH;CACF;CAEA,uBAAgC;EAE9B,AADA,MAAM,qBAAqB,GAC3B,KAAK,cACH,IAAI,YAAY,qBAAqB;GACnC,SAAS;GACT,UAAU;GACV,QAAQ,EAAE,OAAO,KAAK,MAAM;EAC9B,CAAC,CACH;CACF;CAEA,QAAiB,GAAyB;EACxC,CAAI,EAAQ,IAAI,OAAO,KAAK,EAAQ,IAAI,OAAO,KAAK,EAAQ,IAAI,UAAU,MACxE,KAAK,cACH,IAAI,YAAY,iBAAiB;GAC/B,SAAS;GACT,UAAU;EACZ,CAAC,CACH;CAEJ;CAWA,mBAAmC;EACjC,OAAO,CAAC,GAAG,KAAK,UAAU,CAAC,CACxB,QAAO,MAAQ,EAAE,aAAgB,eAAe,EAAK,KAAK,CAAC,CAC3D,KAAI,MAAQ,EAAK,WAAW,CAAC,CAC7B,KAAK,EAAE,CAAC,CACR,KAAK;CACV;CAEA,SAAkB;EAChB,OAAO,CAAI;;qDAEsC,EAAsB;;qCAEtC,KAAK,UAAU,CAAI,qBAAqB,KAAK,mBAAmB,UAAU;;;;CAI7G;AACF;AA5EG,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,SAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS,EAAE,MAAM,OAAO,CAAC,CAAA,GAAA,EAAA,WAAA,SAAA,IAAA,GAYzB,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,GAlB3C,IAAA,EAAA,CAAA,EAAc,eAAe,CAAA,GAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/select/index.ts"],"sourcesContent":["import { html, LitElement, nothing, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport '@/components/icon'\nimport glass from '@/assets/glass.css?inline'\nimport overlayMotion from '@/assets/overlay-motion.css?inline'\nimport type { WebUiOption } from '@/components/option'\nimport { lucideChevronDown } from '@/icons'\nimport { installPointerFocusSuppression } from '@/shared/focus/pointer-focus'\nimport { FormAssociated, defineFormAssociation, FormAssociationController } from '@/shared/form-association'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport {\n createComboboxOpenController,\n createOptionListenerBinding,\n defineOptionPortal,\n handleComboboxFocusOut,\n nextWrappingIndex\n} from '@/shared/option-portal'\nimport { defineAnchoredPanel } from '@/shared/overlay/anchored-panel'\nimport { defineOpenOverlay } from '@/shared/overlay/open-overlay'\nimport {\n applyOverlayVariables,\n defineOverlayPortal,\n type OverlayContainer,\n type OverlayPortal\n} from '@/shared/overlay/portal'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\n\nimport style from './style.css?inline'\n\ninstallPointerFocusSuppression()\n\n@customElement('web-ui-select')\nexport class WebUiSelect extends FormAssociated(LitElement) {\n static override styles = [unsafeCSS(glass), unsafeCSS(overlayMotion), unsafeCSS(style)]\n\n @property({ type: String, reflect: true }) placeholder = ''\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true }) required = false\n @property({ type: Boolean, reflect: true }) portal = false\n @property({ type: Boolean, reflect: true, attribute: 'no-scroll-lock' }) noScrollLock = false\n @property({ attribute: false }) overlayContainer?: OverlayContainer\n @property({ type: String, reflect: true }) name = ''\n\n // value 使用内部状态 + 访问器模式,在变更时同步 ElementInternals;\n // @property + reflect 让它与 input/textarea/slider/input-number 的 value 反射语义保持一致。\n @state() private _value = ''\n @property({ type: String, reflect: true })\n get value(): string {\n return this._value\n }\n set value(v: string) {\n const old = this._value\n this._value = v\n this._formAssociation.sync()\n this.toggleAttribute('data-has-value', !!v)\n this.requestUpdate('value', old)\n }\n\n private readonly _formAssociation = defineFormAssociation<string>({\n host: this,\n initialize: () => {\n // 同步 HTML 属性中预设的 value,补全访问器模式不处理 attribute 反射。\n const attrValue = this.getAttribute('value')\n if (attrValue !== null) {\n this._value = attrValue\n this.toggleAttribute('data-has-value', !!attrValue)\n }\n },\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 syncValidity: () => this._syncValidity()\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n private get _isDisabled(): boolean {\n return this.disabled || this._formAssociation.isFormDisabled()\n }\n\n @state() private _isOpen = false\n @state() private _activeIndex = -1\n @state() private _selectedLabel = ''\n @state() private _hasTriggerSlot = false\n\n private _options: WebUiOption[] = []\n private _portal?: OverlayPortal\n private _portalContent?: HTMLElement\n private readonly _scrollLock = defineScrollLockLease().make()\n\n private static _nextInstanceId = 0\n\n // option 注册表 / portal 同步 / 微任务调度收敛到 shared 模块(autocomplete 同款)\n private readonly _optionPortal = defineOptionPortal().make({\n element: this,\n // 实例级前缀保证多实例同时展开时 option id 不冲突(aria 引用按 id 定位)\n idPrefix: `web-ui-select-${++WebUiSelect._nextInstanceId}`,\n getPortal: () => this._portal,\n getPortalContent: () => this._portalContent,\n isOpen: () => this.portal && this._isOpen,\n getMigratableNodes: () => [...this.querySelectorAll<WebUiOption>('web-ui-option')],\n hasUpdated: () => this.hasUpdated,\n requestUpdate: () => this.requestUpdate(),\n bindOption: option => this._bindOption(option),\n unbindOption: option => this._unbindOption(option),\n onImmediateSync: () => {\n this._refreshOptions()\n this._syncSelected()\n this._syncActiveOption()\n }\n })\n /*\n * 开启态浮层承载「我开着吗」(issue #120 Block 1):本组件不再在 keydown 里自行关闭,\n * 只声明「我开着」。宿主级监听在 portal 模式下收不到面板内的 Escape,正是原实现的\n * 缺陷来源;改由唯一仲裁者在 document 捕获阶段判定最内层。\n *\n * 宿主接口除 requestClose 外只再提供一个 isConnected(仅供惰性回收读取):开启状态是\n * 声明而非询问,仲裁时不再回头问 isOpen()。\n */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => this._close(),\n isConnected: () => this.isConnected\n })\n private readonly _panel = defineAnchoredPanel().make({\n getAnchor: () => this.shadowRoot?.querySelector<HTMLElement>('.select-trigger') ?? null,\n getLocalPanel: () => this.shadowRoot?.querySelector<HTMLElement>('.select-overlay') ?? null,\n getPositioning: () => ({\n placement: 'bottom-start',\n offset: 4,\n // 面板宽度统一为 max(内容, trigger, --wui-overlay-min-width),内容自适应展开\n minAnchorWidth: true,\n strategy: this.portal ? 'fixed' : 'absolute'\n }),\n isPortal: () => this.portal,\n createPortal: () => this._createPortal(),\n openOverlay: this._overlay\n })\n\n get isOpen(): boolean {\n return this._isOpen\n }\n\n get open(): boolean {\n return this._isOpen\n }\n\n private _onClickOutside = (e: MouseEvent) => {\n // 在监听器内部判定:composedPath() 在派发结束后会被清空。\n const isInside = e.composedPath().includes(this) || !!this._panel.getHandle()?.containsEvent(e)\n if (this._isOpen && !isInside) {\n this._close()\n }\n }\n\n private _onFocusOut = () => {\n handleComboboxFocusOut(\n this,\n () => this._panel.getHandle(),\n () => this._isOpen,\n () => this._close()\n )\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._optionPortal.bindHost()\n this.addEventListener('keydown', this._onKeydown)\n this.addEventListener('focusout', this._onFocusOut)\n document.addEventListener('click', this._onClickOutside)\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._optionPortal.dispose()\n this.removeEventListener('keydown', this._onKeydown)\n this.removeEventListener('focusout', this._onFocusOut)\n document.removeEventListener('click', this._onClickOutside)\n this._options.forEach(this._unbindOption)\n this._options = []\n this._portal = undefined\n this._portalContent = undefined\n this._close()\n // 撤销登记由 _panel.dispose() 完成(句柄归它持有),不再需要单独 dispose 仲裁者。\n this._panel.dispose()\n this._scrollLock.release()\n }\n\n override firstUpdated() {\n requestAnimationFrame(() => {\n if (this.isConnected) this._optionPortal.scheduleRefresh()\n })\n }\n\n override willUpdate() {\n this._refreshOptions()\n this._syncSelected()\n }\n\n override updated(changed: PropertyValues) {\n if (changed.has('portal') || changed.has('overlayContainer'))\n requestAnimationFrame(() => this._reconfigureOverlay())\n if (changed.has('noScrollLock')) this._syncScrollLock()\n this._syncOpenAttribute()\n this._syncOverlayInert()\n this._syncValidity()\n }\n\n private _syncValidity() {\n const internals = this._formAssociation.getInternals()\n if (!internals || typeof internals.setValidity !== 'function') return\n if (this._isDisabled || !this.required || this.value) internals.setValidity({})\n else internals.setValidity({ valueMissing: true }, '请选择一项')\n }\n\n private _syncOpenAttribute() {\n this.toggleAttribute('open', this._isOpen)\n }\n\n /*\n * 禁用态不响应 Escape。仲裁者仍在(吞掉按键并压掉原生 cancel),只是不走关闭入口\n * —— 与旧的「跳过候选」语义不同:旧语义放任事件落到下层浮层,把外层一起关掉。\n * 每次渲染后同步即可覆盖 disabled 属性与表单禁用两条来源。\n */\n private _syncOverlayInert() {\n this._panel.getHandle()?.setInert(this._isDisabled)\n }\n\n private _onTriggerSlotChange = (e: Event) => {\n const slot = e.target as HTMLSlotElement\n const hasContent = slot.assignedElements().length > 0\n if (this._hasTriggerSlot === hasContent) return\n this._hasTriggerSlot = hasContent\n this._reconfigureOverlay()\n }\n\n private _syncSelected() {\n this._options.forEach(o => {\n o.selected = o.value === this.value\n })\n if (!this.value) {\n this._selectedLabel = this.placeholder\n return\n }\n const option = this._options.find(o => o.value === this.value)\n this._selectedLabel = option?.label || this.placeholder\n }\n\n private readonly _optionListeners = createOptionListenerBinding({\n // 惰性解引用:handler 字段声明在后者仍可在调用期取到\n onClick: event => this._handleOptionClick(event),\n onPointerOver: event => this._handleOptionPointerOver(event),\n onPointerDown: event => this._handleOptionPointerDown(event),\n onUpdate: () => this._onOptionUpdate()\n })\n private _bindOption = (option: WebUiOption) => this._optionListeners.bind(option)\n private _unbindOption = (option: WebUiOption) => this._optionListeners.unbind(option)\n\n private _refreshOptions() {\n const activeOption = this._options[this._activeIndex]\n // diff 绑定/解绑与激活索引按 option 身份保持在 shared 内完成,\n // 避免移除中间项后激活静默偏移到相邻项\n const next = this._optionPortal.refresh(this._options, activeOption)\n this._options = next.options\n this._activeIndex = next.activeIndex\n // 非 portal 模式 aria-activedescendant 引用真实 option id,需保证已分配\n this._optionPortal.ensureOptionIds(this._options)\n }\n\n private _handleOptionClick = (e: Event) => {\n if (!(e.currentTarget instanceof HTMLElement)) return\n const option = e.currentTarget as WebUiOption\n if (option.disabled || !this._isLive(option)) return\n this.value = option.value\n this._close()\n this._notifyValueChange()\n }\n\n private _onOptionUpdate = () => {\n this._syncSelected()\n this._syncActiveOption()\n }\n\n private _handleOptionPointerOver = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (!this._isOpen || this._activeIndex < 0 || !(e.currentTarget instanceof HTMLElement)) return\n\n // 指针接管后只保留 :hover,避免键盘激活项与悬停项同时高亮。\n this._activeIndex = -1\n this._syncActiveOption()\n }\n\n private _handleOptionPointerDown = (e: PointerEvent) => {\n // 保持 trigger 焦点,避免 focusout 在 click 前关闭浮层。\n e.preventDefault()\n }\n\n // slotchange 不冒泡,只能由模板内 slot 自行监听;稳定引用避免 Lit 重挂载\n private _onSlotChange = () => {\n this._optionPortal.scheduleRefresh()\n }\n\n // slotchange/unregister 的刷新依赖 Lit 渲染调度;键盘或 click 事件可能先于其触发,\n // 已从 DOM 移除或被移出 Select/portal 面板的选项不得再被激活或选中。\n private _isLive(option: WebUiOption): boolean {\n const panel = this.portal ? this._panel.getPanel() : null\n return option.isConnected && (this.contains(option) || (!!panel && panel.contains(option)))\n }\n\n private _onKeydown = (e: KeyboardEvent) => {\n if (this._isDisabled) return\n\n // Escape 不在本组件处理:由共享仲裁者在 document 捕获阶段归属(issue #120 Block 1)。\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault()\n if (!this._isOpen) {\n this._open(true)\n } else {\n this._navigateActive(1)\n }\n break\n case 'ArrowUp':\n e.preventDefault()\n if (!this._isOpen) this._open(true)\n else this._navigateActive(-1)\n break\n case 'Enter': {\n if (this._isOpen && this._activeIndex >= 0) {\n const option = this._options[this._activeIndex]\n if (option && this._isLive(option) && !option.disabled) {\n this.value = option.value\n this._notifyValueChange()\n }\n this._close()\n e.preventDefault()\n }\n break\n }\n }\n }\n\n private _navigateActive(delta: number) {\n const enabled = this._options.filter(o => !o.disabled && this._isLive(o))\n if (!enabled.length) return\n\n const selectedOption = this._options.find(\n option => option.value === this.value && !option.disabled && this._isLive(option)\n )\n const currentIdx =\n this._activeIndex >= 0 && this._isLive(this._options[this._activeIndex])\n ? enabled.indexOf(this._options[this._activeIndex])\n : selectedOption\n ? enabled.indexOf(selectedOption)\n : delta > 0\n ? -1\n : 0\n const nextOption = enabled[nextWrappingIndex(enabled.length, currentIdx, delta)]\n this._activeIndex = this._options.indexOf(nextOption)\n\n this._syncActiveOption()\n }\n\n // 开合生命周期不变量(guard / 派发顺序 / scroll-lock 配对 / portal rAF dance)\n // 收敛在 shared combobox-shell;select 只注入差异点\n private readonly _openController = createComboboxOpenController({\n canOpen: () => !this._isDisabled,\n getIsOpen: () => this._isOpen,\n setIsOpen: open => {\n this._isOpen = open\n },\n dispatchOpenChange: () => this._dispatchOpenChange(),\n syncScrollLock: open => this._syncScrollLock(open),\n isPortal: () => this.portal,\n openOverlay: isKeyboardNavigation => this._openOverlay(isKeyboardNavigation),\n closeOverlay: () => void this._closeOverlay(),\n onOpen: isKeyboardNavigation => {\n this._syncSelected()\n if (isKeyboardNavigation) this._setInitialActiveOption()\n else this._syncActiveOption()\n },\n onAfterClose: () => {\n this._activeIndex = -1\n this._options.forEach(o => o.removeAttribute('active'))\n }\n })\n\n private _open(isKeyboardNavigation = false) {\n this._openController.open(isKeyboardNavigation)\n }\n\n private _close() {\n this._openController.close()\n }\n\n private _dispatchOpenChange() {\n dispatchOpenChangeEvent(this, this._isOpen)\n }\n\n private _setInitialActiveOption() {\n const selectedIndex = this._options.findIndex(\n option => option.value === this.value && !option.disabled && this._isLive(option)\n )\n const firstEnabledIndex = this._options.findIndex(option => !option.disabled && this._isLive(option))\n this._activeIndex = selectedIndex >= 0 ? selectedIndex : firstEnabledIndex\n this._syncActiveOption()\n }\n\n private _syncActiveOption() {\n this._options.forEach((option, index) =>\n option.toggleAttribute('active', index === this._activeIndex && this._isLive(option))\n )\n }\n\n private _notifyValueChange() {\n this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n\n private _syncScrollLock(isOpen = this._isOpen) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n private _togglePopup() {\n if (this._isDisabled) return\n if (this._isOpen) this._close()\n else this._open()\n }\n\n private _openOverlay(isKeyboardNavigation = false) {\n this._panel.open(isKeyboardNavigation)\n // 新会话的句柄刚建立,惰性状态需按当前 disabled 重新同步。\n this._syncOverlayInert()\n }\n\n private _createPortal(): OverlayPortal {\n const portal = defineOverlayPortal().make({\n container: this.overlayContainer,\n target: this,\n style: `${glass}\\n${overlayMotion}\\n${style}`,\n className: 'wui-glass select-overlay portal wui-floating-panel',\n onContentChange: () => this._optionPortal.scheduleRefresh()\n })\n this._portal = portal\n applyOverlayVariables(portal.panel, this, [\n '--wui-overlay-min-width',\n '--wui-select-max-width',\n '--wui-select-max-height'\n ])\n portal.panel.setAttribute('role', 'listbox')\n const scroll = document.createElement('div')\n scroll.className = 'select-scroll'\n const content = document.createElement('div')\n content.className = 'select-content'\n this._portalContent = content\n scroll.append(content)\n portal.panel.append(scroll)\n // 自定义 trigger 必须留在宿主内,不能随 options 迁入 portal 面板\n portal.moveContent(\n Array.from(this.children).filter(child => child.slot !== 'trigger'),\n content\n )\n return portal\n }\n\n private _reconfigureOverlay() {\n // reconfigure 会 dispose 并按需重建 portal,旧引用必须同步失效\n this._portal = undefined\n this._portalContent = undefined\n this._panel.reconfigure(this._isOpen)\n /*\n * reconfigure 重新 claim = 新会话,inert 回到 false。实测这条重推是**冗余**的(摘掉本行\n * 全部用例仍绿):这条路径后面跟着一轮渲染,而 `updated()` 每次渲染都会调\n * `_syncOverlayInert()`。留在这里是不想依赖「这条路径必然跟着一次渲染」这个隐含前提。\n */\n this._syncOverlayInert()\n }\n\n private async _closeOverlay() {\n const closed = await this._panel.close(() => this._isOpen)\n // portal 已随关闭 dispose,与 autocomplete 对齐同步失效引用\n if (closed) {\n this._portal = undefined\n this._portalContent = undefined\n }\n }\n\n override render() {\n // portal 模式下 options 被 moveContent 移入浮层 shadow root,id 引用无法跨树作用域\n // 解析,因此 activedescendant 仅在非 portal 模式输出;aria-controls 同理指向同根内 listbox\n const listboxId = `${this.localName}-listbox`\n const activeDescendant =\n !this.portal && this._isOpen && this._activeIndex >= 0\n ? (this._options[this._activeIndex]?.id ?? nothing)\n : nothing\n\n return html`\n <div class=\"wui-select-inner\">\n <div\n class=\"wui-glass select-trigger\"\n ?data-custom-trigger=${this._hasTriggerSlot}\n @click=${this._togglePopup}\n tabindex=${this._isDisabled ? '-1' : '0'}\n role=\"combobox\"\n aria-expanded=${this._isOpen}\n aria-haspopup=\"listbox\"\n aria-controls=${this.portal ? nothing : listboxId}\n aria-disabled=${String(this._isDisabled)}\n aria-activedescendant=${activeDescendant}\n >\n <slot name=\"trigger\" @slotchange=${this._onTriggerSlotChange}></slot>\n ${!this._hasTriggerSlot ? html`<span class=\"label\">${this._selectedLabel}</span>` : nothing}\n <web-ui-icon class=\"arrow\" .icon=${lucideChevronDown}></web-ui-icon>\n </div>\n <div class=\"select-overlay wui-floating-panel wui-glass\" hidden role=\"listbox\" id=${listboxId}>\n <div class=\"select-scroll\">\n <div class=\"select-content\">\n <slot @slotchange=${this._onSlotChange}></slot>\n </div>\n </div>\n </div>\n </div>\n `\n }\n\n declare readonly $events: {\n input: Event\n change: Event\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-select': WebUiSelect\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/select/index.ts"],"sourcesContent":["import '@/components/icon'\n\nimport { html, LitElement, nothing, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport glass from '@/assets/glass.css?inline'\nimport overlayMotion from '@/assets/overlay-motion.css?inline'\nimport type { WebUiOption } from '@/components/option'\nimport { lucideChevronDown } from '@/icons'\nimport { installPointerFocusSuppression } from '@/shared/focus/pointer-focus'\nimport { FormAssociated, defineFormAssociation, FormAssociationController } from '@/shared/form-association'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport {\n createComboboxOpenController,\n createOptionListenerBinding,\n defineOptionPortal,\n handleComboboxFocusOut,\n nextWrappingIndex\n} from '@/shared/option-portal'\nimport { defineAnchoredPanel } from '@/shared/overlay/anchored-panel'\nimport { defineOpenOverlay } from '@/shared/overlay/open-overlay'\nimport {\n applyOverlayVariables,\n defineOverlayPortal,\n type OverlayContainer,\n type OverlayPortal\n} from '@/shared/overlay/portal'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\n\nimport style from './style.css?inline'\n\ninstallPointerFocusSuppression()\n\n@customElement('web-ui-select')\nexport class WebUiSelect extends FormAssociated(LitElement) {\n static override styles = [unsafeCSS(glass), unsafeCSS(overlayMotion), unsafeCSS(style)]\n\n @property({ type: String, reflect: true }) placeholder = ''\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true }) required = false\n @property({ type: Boolean, reflect: true }) portal = false\n @property({ type: Boolean, reflect: true, attribute: 'no-scroll-lock' }) noScrollLock = false\n @property({ attribute: false }) overlayContainer?: OverlayContainer\n @property({ type: String, reflect: true }) name = ''\n\n // value 使用内部状态 + 访问器模式,在变更时同步 ElementInternals;\n // @property + reflect 让它与 input/textarea/slider/input-number 的 value 反射语义保持一致。\n @state() private _value = ''\n @property({ type: String, reflect: true })\n get value(): string {\n return this._value\n }\n set value(v: string) {\n const old = this._value\n this._value = v\n this._formAssociation.sync()\n this.toggleAttribute('data-has-value', !!v)\n this.requestUpdate('value', old)\n }\n\n private readonly _formAssociation = defineFormAssociation<string>({\n host: this,\n initialize: () => {\n // 同步 HTML 属性中预设的 value,补全访问器模式不处理 attribute 反射。\n const attrValue = this.getAttribute('value')\n if (attrValue !== null) {\n this._value = attrValue\n this.toggleAttribute('data-has-value', !!attrValue)\n }\n },\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 syncValidity: () => this._syncValidity()\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n private get _isDisabled(): boolean {\n return this.disabled || this._formAssociation.isFormDisabled()\n }\n\n @state() private _isOpen = false\n @state() private _activeIndex = -1\n @state() private _selectedLabel = ''\n @state() private _hasTriggerSlot = false\n\n private _options: WebUiOption[] = []\n private _portal?: OverlayPortal\n private _portalContent?: HTMLElement\n private readonly _scrollLock = defineScrollLockLease().make()\n\n private static _nextInstanceId = 0\n\n // option 注册表 / portal 同步 / 微任务调度收敛到 shared 模块(autocomplete 同款)\n private readonly _optionPortal = defineOptionPortal().make({\n element: this,\n // 实例级前缀保证多实例同时展开时 option id 不冲突(aria 引用按 id 定位)\n idPrefix: `web-ui-select-${++WebUiSelect._nextInstanceId}`,\n getPortal: () => this._portal,\n getPortalContent: () => this._portalContent,\n isOpen: () => this.portal && this._isOpen,\n getMigratableNodes: () => [...this.querySelectorAll<WebUiOption>('web-ui-option')],\n hasUpdated: () => this.hasUpdated,\n requestUpdate: () => this.requestUpdate(),\n bindOption: option => this._bindOption(option),\n unbindOption: option => this._unbindOption(option),\n onImmediateSync: () => {\n this._refreshOptions()\n this._syncSelected()\n this._syncActiveOption()\n }\n })\n /*\n * 开启态浮层承载「我开着吗」(issue #120 Block 1):本组件不再在 keydown 里自行关闭,\n * 只声明「我开着」。宿主级监听在 portal 模式下收不到面板内的 Escape,正是原实现的\n * 缺陷来源;改由唯一仲裁者在 document 捕获阶段判定最内层。\n *\n * 宿主接口除 requestClose 外只再提供一个 isConnected(仅供惰性回收读取):开启状态是\n * 声明而非询问,仲裁时不再回头问 isOpen()。\n */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => this._close(),\n isConnected: () => this.isConnected\n })\n private readonly _panel = defineAnchoredPanel().make({\n getAnchor: () => this.shadowRoot?.querySelector<HTMLElement>('.select-trigger') ?? null,\n getLocalPanel: () => this.shadowRoot?.querySelector<HTMLElement>('.select-overlay') ?? null,\n getPositioning: () => ({\n placement: 'bottom-start',\n offset: 4,\n // 面板宽度统一为 max(内容, trigger, --wui-overlay-min-width),内容自适应展开\n minAnchorWidth: true,\n strategy: this.portal ? 'fixed' : 'absolute'\n }),\n isPortal: () => this.portal,\n createPortal: () => this._createPortal(),\n openOverlay: this._overlay\n })\n\n get isOpen(): boolean {\n return this._isOpen\n }\n\n get open(): boolean {\n return this._isOpen\n }\n\n private _onClickOutside = (e: MouseEvent) => {\n // 在监听器内部判定:composedPath() 在派发结束后会被清空。\n const isInside = e.composedPath().includes(this) || !!this._panel.getHandle()?.containsEvent(e)\n if (this._isOpen && !isInside) {\n this._close()\n }\n }\n\n private _onFocusOut = () => {\n handleComboboxFocusOut(\n this,\n () => this._panel.getHandle(),\n () => this._isOpen,\n () => this._close()\n )\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._optionPortal.bindHost()\n this.addEventListener('keydown', this._onKeydown)\n this.addEventListener('focusout', this._onFocusOut)\n document.addEventListener('click', this._onClickOutside)\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._optionPortal.dispose()\n this.removeEventListener('keydown', this._onKeydown)\n this.removeEventListener('focusout', this._onFocusOut)\n document.removeEventListener('click', this._onClickOutside)\n this._options.forEach(this._unbindOption)\n this._options = []\n this._portal = undefined\n this._portalContent = undefined\n this._close()\n // 撤销登记由 _panel.dispose() 完成(句柄归它持有),不再需要单独 dispose 仲裁者。\n this._panel.dispose()\n this._scrollLock.release()\n }\n\n override firstUpdated() {\n requestAnimationFrame(() => {\n if (this.isConnected) this._optionPortal.scheduleRefresh()\n })\n }\n\n override willUpdate() {\n this._refreshOptions()\n this._syncSelected()\n }\n\n override updated(changed: PropertyValues) {\n if (changed.has('portal') || changed.has('overlayContainer'))\n requestAnimationFrame(() => this._reconfigureOverlay())\n if (changed.has('noScrollLock')) this._syncScrollLock()\n this._syncOpenAttribute()\n this._syncOverlayInert()\n this._syncValidity()\n }\n\n private _syncValidity() {\n const internals = this._formAssociation.getInternals()\n if (!internals || typeof internals.setValidity !== 'function') return\n if (this._isDisabled || !this.required || this.value) internals.setValidity({})\n else internals.setValidity({ valueMissing: true }, '请选择一项')\n }\n\n private _syncOpenAttribute() {\n this.toggleAttribute('open', this._isOpen)\n }\n\n /*\n * 禁用态不响应 Escape。仲裁者仍在(吞掉按键并压掉原生 cancel),只是不走关闭入口\n * —— 与旧的「跳过候选」语义不同:旧语义放任事件落到下层浮层,把外层一起关掉。\n * 每次渲染后同步即可覆盖 disabled 属性与表单禁用两条来源。\n */\n private _syncOverlayInert() {\n this._panel.getHandle()?.setInert(this._isDisabled)\n }\n\n private _onTriggerSlotChange = (e: Event) => {\n const slot = e.target as HTMLSlotElement\n const hasContent = slot.assignedElements().length > 0\n if (this._hasTriggerSlot === hasContent) return\n this._hasTriggerSlot = hasContent\n this._reconfigureOverlay()\n }\n\n private _syncSelected() {\n this._options.forEach(o => {\n o.selected = o.value === this.value\n })\n if (!this.value) {\n this._selectedLabel = this.placeholder\n return\n }\n const option = this._options.find(o => o.value === this.value)\n this._selectedLabel = option?.label || this.placeholder\n }\n\n private readonly _optionListeners = createOptionListenerBinding({\n // 惰性解引用:handler 字段声明在后者仍可在调用期取到\n onClick: event => this._handleOptionClick(event),\n onPointerOver: event => this._handleOptionPointerOver(event),\n onPointerDown: event => this._handleOptionPointerDown(event),\n onUpdate: () => this._onOptionUpdate()\n })\n private _bindOption = (option: WebUiOption) => this._optionListeners.bind(option)\n private _unbindOption = (option: WebUiOption) => this._optionListeners.unbind(option)\n\n private _refreshOptions() {\n const activeOption = this._options[this._activeIndex]\n // diff 绑定/解绑与激活索引按 option 身份保持在 shared 内完成,\n // 避免移除中间项后激活静默偏移到相邻项\n const next = this._optionPortal.refresh(this._options, activeOption)\n this._options = next.options\n this._activeIndex = next.activeIndex\n // 非 portal 模式 aria-activedescendant 引用真实 option id,需保证已分配\n this._optionPortal.ensureOptionIds(this._options)\n }\n\n private _handleOptionClick = (e: Event) => {\n if (!(e.currentTarget instanceof HTMLElement)) return\n const option = e.currentTarget as WebUiOption\n if (option.disabled || !this._isLive(option)) return\n this.value = option.value\n this._close()\n this._notifyValueChange()\n }\n\n private _onOptionUpdate = () => {\n this._syncSelected()\n this._syncActiveOption()\n }\n\n private _handleOptionPointerOver = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (!this._isOpen || this._activeIndex < 0 || !(e.currentTarget instanceof HTMLElement)) return\n\n // 指针接管后只保留 :hover,避免键盘激活项与悬停项同时高亮。\n this._activeIndex = -1\n this._syncActiveOption()\n }\n\n private _handleOptionPointerDown = (e: PointerEvent) => {\n // 保持 trigger 焦点,避免 focusout 在 click 前关闭浮层。\n e.preventDefault()\n }\n\n // slotchange 不冒泡,只能由模板内 slot 自行监听;稳定引用避免 Lit 重挂载\n private _onSlotChange = () => {\n this._optionPortal.scheduleRefresh()\n }\n\n // slotchange/unregister 的刷新依赖 Lit 渲染调度;键盘或 click 事件可能先于其触发,\n // 已从 DOM 移除或被移出 Select/portal 面板的选项不得再被激活或选中。\n private _isLive(option: WebUiOption): boolean {\n const panel = this.portal ? this._panel.getPanel() : null\n return option.isConnected && (this.contains(option) || (!!panel && panel.contains(option)))\n }\n\n private _onKeydown = (e: KeyboardEvent) => {\n if (this._isDisabled) return\n\n // Escape 不在本组件处理:由共享仲裁者在 document 捕获阶段归属(issue #120 Block 1)。\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault()\n if (!this._isOpen) {\n this._open(true)\n } else {\n this._navigateActive(1)\n }\n break\n case 'ArrowUp':\n e.preventDefault()\n if (!this._isOpen) this._open(true)\n else this._navigateActive(-1)\n break\n case 'Enter': {\n if (this._isOpen && this._activeIndex >= 0) {\n const option = this._options[this._activeIndex]\n if (option && this._isLive(option) && !option.disabled) {\n this.value = option.value\n this._notifyValueChange()\n }\n this._close()\n e.preventDefault()\n }\n break\n }\n }\n }\n\n private _navigateActive(delta: number) {\n const enabled = this._options.filter(o => !o.disabled && this._isLive(o))\n if (!enabled.length) return\n\n const selectedOption = this._options.find(\n option => option.value === this.value && !option.disabled && this._isLive(option)\n )\n const currentIdx =\n this._activeIndex >= 0 && this._isLive(this._options[this._activeIndex])\n ? enabled.indexOf(this._options[this._activeIndex])\n : selectedOption\n ? enabled.indexOf(selectedOption)\n : delta > 0\n ? -1\n : 0\n const nextOption = enabled[nextWrappingIndex(enabled.length, currentIdx, delta)]\n this._activeIndex = this._options.indexOf(nextOption)\n\n this._syncActiveOption()\n }\n\n // 开合生命周期不变量(guard / 派发顺序 / scroll-lock 配对 / portal rAF dance)\n // 收敛在 shared combobox-shell;select 只注入差异点\n private readonly _openController = createComboboxOpenController({\n canOpen: () => !this._isDisabled,\n getIsOpen: () => this._isOpen,\n setIsOpen: open => {\n this._isOpen = open\n },\n dispatchOpenChange: () => this._dispatchOpenChange(),\n syncScrollLock: open => this._syncScrollLock(open),\n isPortal: () => this.portal,\n openOverlay: isKeyboardNavigation => this._openOverlay(isKeyboardNavigation),\n closeOverlay: () => void this._closeOverlay(),\n onOpen: isKeyboardNavigation => {\n this._syncSelected()\n if (isKeyboardNavigation) this._setInitialActiveOption()\n else this._syncActiveOption()\n },\n onAfterClose: () => {\n this._activeIndex = -1\n this._options.forEach(o => o.removeAttribute('active'))\n }\n })\n\n private _open(isKeyboardNavigation = false) {\n this._openController.open(isKeyboardNavigation)\n }\n\n private _close() {\n this._openController.close()\n }\n\n private _dispatchOpenChange() {\n dispatchOpenChangeEvent(this, this._isOpen)\n }\n\n private _setInitialActiveOption() {\n const selectedIndex = this._options.findIndex(\n option => option.value === this.value && !option.disabled && this._isLive(option)\n )\n const firstEnabledIndex = this._options.findIndex(option => !option.disabled && this._isLive(option))\n this._activeIndex = selectedIndex >= 0 ? selectedIndex : firstEnabledIndex\n this._syncActiveOption()\n }\n\n private _syncActiveOption() {\n this._options.forEach((option, index) =>\n option.toggleAttribute('active', index === this._activeIndex && this._isLive(option))\n )\n }\n\n private _notifyValueChange() {\n this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n\n private _syncScrollLock(isOpen = this._isOpen) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n private _togglePopup() {\n if (this._isDisabled) return\n if (this._isOpen) this._close()\n else this._open()\n }\n\n private _openOverlay(isKeyboardNavigation = false) {\n this._panel.open(isKeyboardNavigation)\n // 新会话的句柄刚建立,惰性状态需按当前 disabled 重新同步。\n this._syncOverlayInert()\n }\n\n private _createPortal(): OverlayPortal {\n const portal = defineOverlayPortal().make({\n container: this.overlayContainer,\n target: this,\n style: `${glass}\\n${overlayMotion}\\n${style}`,\n className: 'wui-glass select-overlay portal wui-floating-panel',\n onContentChange: () => this._optionPortal.scheduleRefresh()\n })\n this._portal = portal\n applyOverlayVariables(portal.panel, this, [\n '--wui-overlay-min-width',\n '--wui-select-max-width',\n '--wui-select-max-height'\n ])\n portal.panel.setAttribute('role', 'listbox')\n const scroll = document.createElement('div')\n scroll.className = 'select-scroll'\n const content = document.createElement('div')\n content.className = 'select-content'\n this._portalContent = content\n scroll.append(content)\n portal.panel.append(scroll)\n // 自定义 trigger 必须留在宿主内,不能随 options 迁入 portal 面板\n portal.moveContent(\n Array.from(this.children).filter(child => child.slot !== 'trigger'),\n content\n )\n return portal\n }\n\n private _reconfigureOverlay() {\n // reconfigure 会 dispose 并按需重建 portal,旧引用必须同步失效\n this._portal = undefined\n this._portalContent = undefined\n this._panel.reconfigure(this._isOpen)\n /*\n * reconfigure 重新 claim = 新会话,inert 回到 false。实测这条重推是**冗余**的(摘掉本行\n * 全部用例仍绿):这条路径后面跟着一轮渲染,而 `updated()` 每次渲染都会调\n * `_syncOverlayInert()`。留在这里是不想依赖「这条路径必然跟着一次渲染」这个隐含前提。\n */\n this._syncOverlayInert()\n }\n\n private async _closeOverlay() {\n const closed = await this._panel.close(() => this._isOpen)\n // portal 已随关闭 dispose,与 autocomplete 对齐同步失效引用\n if (closed) {\n this._portal = undefined\n this._portalContent = undefined\n }\n }\n\n override render() {\n // portal 模式下 options 被 moveContent 移入浮层 shadow root,id 引用无法跨树作用域\n // 解析,因此 activedescendant 仅在非 portal 模式输出;aria-controls 同理指向同根内 listbox\n const listboxId = `${this.localName}-listbox`\n const activeDescendant =\n !this.portal && this._isOpen && this._activeIndex >= 0\n ? (this._options[this._activeIndex]?.id ?? nothing)\n : nothing\n\n return html`\n <div class=\"wui-select-inner\">\n <div\n class=\"wui-glass select-trigger\"\n ?data-custom-trigger=${this._hasTriggerSlot}\n @click=${this._togglePopup}\n tabindex=${this._isDisabled ? '-1' : '0'}\n role=\"combobox\"\n aria-expanded=${this._isOpen}\n aria-haspopup=\"listbox\"\n aria-controls=${this.portal ? nothing : listboxId}\n aria-disabled=${String(this._isDisabled)}\n aria-activedescendant=${activeDescendant}\n >\n <slot name=\"trigger\" @slotchange=${this._onTriggerSlotChange}></slot>\n ${!this._hasTriggerSlot ? html`<span class=\"label\">${this._selectedLabel}</span>` : nothing}\n <web-ui-icon class=\"arrow\" .icon=${lucideChevronDown}></web-ui-icon>\n </div>\n <div class=\"select-overlay wui-floating-panel wui-glass\" hidden role=\"listbox\" id=${listboxId}>\n <div class=\"select-scroll\">\n <div class=\"select-content\">\n <slot @slotchange=${this._onSlotChange}></slot>\n </div>\n </div>\n </div>\n </div>\n `\n }\n\n declare readonly $events: {\n input: Event\n change: Event\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-select': WebUiSelect\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA+BA,EAA+B;AAGxB,IAAM,IAAN,cAA0B,EAAe,CAAU,CAAC,CAAC;;;;;EAkVvB,aA/UsB,KAAA,cAAA,IACF,KAAA,WAAA,IACA,KAAA,WAAA,IACF,KAAA,SAAA,IACmC,KAAA,eAAA,IAEtC,KAAA,OAAA,IAIxB,KAAA,SAAA,IAaU,KAAA,mBAAA,EAA8B;GAChE,MAAM;GACN,kBAAkB;IAEhB,IAAM,IAAY,KAAK,aAAa,OAAO;IAC3C,AAAI,MAAc,SAChB,KAAK,SAAS,GACd,KAAK,gBAAgB,kBAAkB,CAAC,CAAC,CAAS;GAEtD;GACA,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;GACA,oBAAoB,KAAK,cAAc;EACzC,CAAC,CAAC,CAAC,KAAK,GAEsC,KAAA,6BAAA,IAAI,EAA0B,MAAM,KAAK,gBAAgB,GAM5E,KAAA,UAAA,IACK,KAAA,eAAA,IACE,KAAA,iBAAA,IACC,KAAA,kBAAA,IAED,KAAA,WAAA,CAAC,GAGJ,KAAA,cAAA,EAAsB,CAAC,CAAC,KAAK,GAK3B,KAAA,gBAAA,EAAmB,CAAC,CAAC,KAAK;GACzD,SAAS;GAET,UAAU,iBAAiB,EAAA,EAAc;GACzC,iBAAiB,KAAK;GACtB,wBAAwB,KAAK;GAC7B,cAAc,KAAK,UAAU,KAAK;GAClC,0BAA0B,CAAC,GAAG,KAAK,iBAA8B,eAAe,CAAC;GACjF,kBAAkB,KAAK;GACvB,qBAAqB,KAAK,cAAc;GACxC,aAAY,MAAU,KAAK,YAAY,CAAM;GAC7C,eAAc,MAAU,KAAK,cAAc,CAAM;GACjD,uBAAuB;IAGrB,AAFA,KAAK,gBAAgB,GACrB,KAAK,cAAc,GACnB,KAAK,kBAAkB;GACzB;EACF,CAAC,GAS2B,KAAA,WAAA,EAAkB,CAAC,CAAC,KAAK;GACnD,oBAAoB,KAAK,OAAO;GAChC,mBAAmB,KAAK;EAC1B,CAAC,GACyB,KAAA,SAAA,EAAoB,CAAC,CAAC,KAAK;GACnD,iBAAiB,KAAK,YAAY,cAA2B,iBAAiB,KAAK;GACnF,qBAAqB,KAAK,YAAY,cAA2B,iBAAiB,KAAK;GACvF,uBAAuB;IACrB,WAAW;IACX,QAAQ;IAER,gBAAgB;IAChB,UAAU,KAAK,SAAS,UAAU;GACpC;GACA,gBAAgB,KAAK;GACrB,oBAAoB,KAAK,cAAc;GACvC,aAAa,KAAK;EACpB,CAAC,GAU0B,KAAA,mBAAA,MAAkB;GAE3C,IAAM,IAAW,EAAE,aAAa,CAAC,CAAC,SAAS,IAAI,KAAK,CAAC,CAAC,KAAK,OAAO,UAAU,CAAC,EAAE,cAAc,CAAC;GAC9F,AAAI,KAAK,WAAW,CAAC,KACnB,KAAK,OAAO;EAEhB,GAE4B,KAAA,oBAAA;GAC1B,EACE,YACM,KAAK,OAAO,UAAU,SACtB,KAAK,eACL,KAAK,OAAO,CACpB;EACF,GAkEgC,KAAA,wBAAA,MAAa;GAE3C,IAAM,IADO,EAAE,OACS,iBAAiB,CAAC,CAAC,SAAS;GAChD,KAAK,oBAAoB,MAC7B,KAAK,kBAAkB,GACvB,KAAK,oBAAoB;EAC3B,GAcoC,KAAA,mBAAA,EAA4B;GAE9D,UAAS,MAAS,KAAK,mBAAmB,CAAK;GAC/C,gBAAe,MAAS,KAAK,yBAAyB,CAAK;GAC3D,gBAAe,MAAS,KAAK,yBAAyB,CAAK;GAC3D,gBAAgB,KAAK,gBAAgB;EACvC,CAAC,GACsB,KAAA,eAAA,MAAwB,KAAK,iBAAiB,KAAK,CAAM,GACvD,KAAA,iBAAA,MAAwB,KAAK,iBAAiB,OAAO,CAAM,GAatD,KAAA,sBAAA,MAAa;GACzC,IAAI,EAAE,EAAE,yBAAyB,cAAc;GAC/C,IAAM,IAAS,EAAE;GACb,GAAO,YAAa,KAAK,QAAQ,CAAM,MAC3C,KAAK,QAAQ,EAAO,OACpB,KAAK,OAAO,GACZ,KAAK,mBAAmB;EAC1B,GAEgC,KAAA,wBAAA;GAE9B,AADA,KAAK,cAAc,GACnB,KAAK,kBAAkB;EACzB,GAEoC,KAAA,4BAAA,MAAoB;GAClD,EAAE,gBAAgB,YAClB,CAAC,KAAK,WAAW,KAAK,eAAe,KAAK,EAAE,EAAE,yBAAyB,iBAG3E,KAAK,eAAe,IACpB,KAAK,kBAAkB;EACzB,GAEoC,KAAA,4BAAA,MAAoB;GAEtD,EAAE,eAAe;EACnB,GAG8B,KAAA,sBAAA;GAC5B,KAAK,cAAc,gBAAgB;EACrC,GASsB,KAAA,cAAA,MAAqB;GACrC,UAAK,aAGT,QAAQ,EAAE,KAAV;IACE,KAAK;KAEH,AADA,EAAE,eAAe,GACZ,KAAK,UAGR,KAAK,gBAAgB,CAAC,IAFtB,KAAK,MAAM,EAAI;KAIjB;IACF,KAAK;KAEH,AADA,EAAE,eAAe,GACZ,KAAK,UACL,KAAK,gBAAgB,EAAE,IADT,KAAK,MAAM,EAAI;KAElC;IACF,KAAK,SACH,IAAI,KAAK,WAAW,KAAK,gBAAgB,GAAG;KAC1C,IAAM,IAAS,KAAK,SAAS,KAAK;KAMlC,AALI,KAAU,KAAK,QAAQ,CAAM,KAAK,CAAC,EAAO,aAC5C,KAAK,QAAQ,EAAO,OACpB,KAAK,mBAAmB,IAE1B,KAAK,OAAO,GACZ,EAAE,eAAe;IACnB;GAGJ;EACF,GAyBmC,KAAA,kBAAA,EAA6B;GAC9D,eAAe,CAAC,KAAK;GACrB,iBAAiB,KAAK;GACtB,YAAW,MAAQ;IACjB,KAAK,UAAU;GACjB;GACA,0BAA0B,KAAK,oBAAoB;GACnD,iBAAgB,MAAQ,KAAK,gBAAgB,CAAI;GACjD,gBAAgB,KAAK;GACrB,cAAa,MAAwB,KAAK,aAAa,CAAoB;GAC3E,oBAAoB,KAAK,KAAK,cAAc;GAC5C,SAAQ,MAAwB;IAE9B,AADA,KAAK,cAAc,GACf,IAAsB,KAAK,wBAAwB,IAClD,KAAK,kBAAkB;GAC9B;GACA,oBAAoB;IAElB,AADA,KAAK,eAAe,IACpB,KAAK,SAAS,SAAQ,MAAK,EAAE,gBAAgB,QAAQ,CAAC;GACxD;EACF,CAAC;;;EArWwB,KAAA,SAAA;GAAC,EAAU,CAAK;GAAG,EAAU,CAAa;GAAG,EAAU,CAAK;EAAC;;CAatF,IACI,QAAgB;EAClB,OAAO,KAAK;CACd;CACA,IAAI,MAAM,GAAW;EACnB,IAAM,IAAM,KAAK;EAIjB,AAHA,KAAK,SAAS,GACd,KAAK,iBAAiB,KAAK,GAC3B,KAAK,gBAAgB,kBAAkB,CAAC,CAAC,CAAC,GAC1C,KAAK,cAAc,SAAS,CAAG;CACjC;CA0BA,IAAY,cAAuB;EACjC,OAAO,KAAK,YAAY,KAAK,iBAAiB,eAAe;CAC/D;;EAYiC,KAAA,kBAAA;;CAgDjC,IAAI,SAAkB;EACpB,OAAO,KAAK;CACd;CAEA,IAAI,OAAgB;EAClB,OAAO,KAAK;CACd;CAmBA,oBAA6B;EAK3B,AAJA,MAAM,kBAAkB,GACxB,KAAK,cAAc,SAAS,GAC5B,KAAK,iBAAiB,WAAW,KAAK,UAAU,GAChD,KAAK,iBAAiB,YAAY,KAAK,WAAW,GAClD,SAAS,iBAAiB,SAAS,KAAK,eAAe;CACzD;CAEA,uBAAgC;EAa9B,AAZA,MAAM,qBAAqB,GAC3B,KAAK,cAAc,QAAQ,GAC3B,KAAK,oBAAoB,WAAW,KAAK,UAAU,GACnD,KAAK,oBAAoB,YAAY,KAAK,WAAW,GACrD,SAAS,oBAAoB,SAAS,KAAK,eAAe,GAC1D,KAAK,SAAS,QAAQ,KAAK,aAAa,GACxC,KAAK,WAAW,CAAC,GACjB,KAAK,UAAU,KAAA,GACf,KAAK,iBAAiB,KAAA,GACtB,KAAK,OAAO,GAEZ,KAAK,OAAO,QAAQ,GACpB,KAAK,YAAY,QAAQ;CAC3B;CAEA,eAAwB;EACtB,4BAA4B;GAC1B,AAAI,KAAK,eAAa,KAAK,cAAc,gBAAgB;EAC3D,CAAC;CACH;CAEA,aAAsB;EAEpB,AADA,KAAK,gBAAgB,GACrB,KAAK,cAAc;CACrB;CAEA,QAAiB,GAAyB;EAMxC,CALI,EAAQ,IAAI,QAAQ,KAAK,EAAQ,IAAI,kBAAkB,MACzD,4BAA4B,KAAK,oBAAoB,CAAC,GACpD,EAAQ,IAAI,cAAc,KAAG,KAAK,gBAAgB,GACtD,KAAK,mBAAmB,GACxB,KAAK,kBAAkB,GACvB,KAAK,cAAc;CACrB;CAEA,gBAAwB;EACtB,IAAM,IAAY,KAAK,iBAAiB,aAAa;EACjD,AAAC,KAAa,OAAO,EAAU,eAAgB,eAC/C,KAAK,eAAe,CAAC,KAAK,YAAY,KAAK,QAAO,EAAU,YAAY,CAAC,CAAC,IACzE,EAAU,YAAY,EAAE,cAAc,GAAK,GAAG,OAAO;CAC5D;CAEA,qBAA6B;EAC3B,KAAK,gBAAgB,QAAQ,KAAK,OAAO;CAC3C;CAOA,oBAA4B;EAC1B,KAAK,OAAO,UAAU,CAAC,EAAE,SAAS,KAAK,WAAW;CACpD;CAUA,gBAAwB;EAItB,IAHA,KAAK,SAAS,SAAQ,MAAK;GACzB,EAAE,WAAW,EAAE,UAAU,KAAK;EAChC,CAAC,GACG,CAAC,KAAK,OAAO;GACf,KAAK,iBAAiB,KAAK;GAC3B;EACF;EACA,IAAM,IAAS,KAAK,SAAS,MAAK,MAAK,EAAE,UAAU,KAAK,KAAK;EAC7D,KAAK,iBAAiB,GAAQ,SAAS,KAAK;CAC9C;CAYA,kBAA0B;EACxB,IAAM,IAAe,KAAK,SAAS,KAAK,eAGlC,IAAO,KAAK,cAAc,QAAQ,KAAK,UAAU,CAAY;EAInE,AAHA,KAAK,WAAW,EAAK,SACrB,KAAK,eAAe,EAAK,aAEzB,KAAK,cAAc,gBAAgB,KAAK,QAAQ;CAClD;CAqCA,QAAgB,GAA8B;EAC5C,IAAM,IAAQ,KAAK,SAAS,KAAK,OAAO,SAAS,IAAI;EACrD,OAAO,EAAO,gBAAgB,KAAK,SAAS,CAAM,KAAM,CAAC,CAAC,KAAS,EAAM,SAAS,CAAM;CAC1F;CAmCA,gBAAwB,GAAe;EACrC,IAAM,IAAU,KAAK,SAAS,QAAO,MAAK,CAAC,EAAE,YAAY,KAAK,QAAQ,CAAC,CAAC;EACxE,IAAI,CAAC,EAAQ,QAAQ;EAErB,IAAM,IAAiB,KAAK,SAAS,MACnC,MAAU,EAAO,UAAU,KAAK,SAAS,CAAC,EAAO,YAAY,KAAK,QAAQ,CAAM,CAClF,GACM,IACJ,KAAK,gBAAgB,KAAK,KAAK,QAAQ,KAAK,SAAS,KAAK,aAAa,IACnE,EAAQ,QAAQ,KAAK,SAAS,KAAK,aAAa,IAChD,IACE,EAAQ,QAAQ,CAAc,IAC9B,IAAQ,IACN,KACA,GACJ,IAAa,EAAQ,EAAkB,EAAQ,QAAQ,GAAY,CAAK;EAG9E,AAFA,KAAK,eAAe,KAAK,SAAS,QAAQ,CAAU,GAEpD,KAAK,kBAAkB;CACzB;CA0BA,MAAc,IAAuB,IAAO;EAC1C,KAAK,gBAAgB,KAAK,CAAoB;CAChD;CAEA,SAAiB;EACf,KAAK,gBAAgB,MAAM;CAC7B;CAEA,sBAA8B;EAC5B,EAAwB,MAAM,KAAK,OAAO;CAC5C;CAEA,0BAAkC;EAChC,IAAM,IAAgB,KAAK,SAAS,WAClC,MAAU,EAAO,UAAU,KAAK,SAAS,CAAC,EAAO,YAAY,KAAK,QAAQ,CAAM,CAClF,GACM,IAAoB,KAAK,SAAS,WAAU,MAAU,CAAC,EAAO,YAAY,KAAK,QAAQ,CAAM,CAAC;EAEpG,AADA,KAAK,eAAe,KAAiB,IAAI,IAAgB,GACzD,KAAK,kBAAkB;CACzB;CAEA,oBAA4B;EAC1B,KAAK,SAAS,SAAS,GAAQ,MAC7B,EAAO,gBAAgB,UAAU,MAAU,KAAK,gBAAgB,KAAK,QAAQ,CAAM,CAAC,CACtF;CACF;CAEA,qBAA6B;EAE3B,AADA,KAAK,cAAc,IAAI,MAAM,SAAS;GAAE,SAAS;GAAM,UAAU;EAAK,CAAC,CAAC,GACxE,KAAK,cAAc,IAAI,MAAM,UAAU;GAAE,SAAS;GAAM,UAAU;EAAK,CAAC,CAAC;CAC3E;CAEA,gBAAwB,IAAS,KAAK,SAAS;EAC7C,KAAK,YAAY,KAAK,KAAU,CAAC,KAAK,YAAY;CACpD;CAEA,eAAuB;EACjB,KAAK,gBACL,KAAK,UAAS,KAAK,OAAO,IACzB,KAAK,MAAM;CAClB;CAEA,aAAqB,IAAuB,IAAO;EAGjD,AAFA,KAAK,OAAO,KAAK,CAAoB,GAErC,KAAK,kBAAkB;CACzB;CAEA,gBAAuC;EACrC,IAAM,IAAS,EAAoB,CAAC,CAAC,KAAK;GACxC,WAAW,KAAK;GAChB,QAAQ;GACR,OAAO,GAAG,EAAM,IAAI,EAAc,IAAI;GACtC,WAAW;GACX,uBAAuB,KAAK,cAAc,gBAAgB;EAC5D,CAAC;EAOD,AANA,KAAK,UAAU,GACf,EAAsB,EAAO,OAAO,MAAM;GACxC;GACA;GACA;EACF,CAAC,GACD,EAAO,MAAM,aAAa,QAAQ,SAAS;EAC3C,IAAM,IAAS,SAAS,cAAc,KAAK;EAC3C,EAAO,YAAY;EACnB,IAAM,IAAU,SAAS,cAAc,KAAK;EAU5C,OATA,EAAQ,YAAY,kBACpB,KAAK,iBAAiB,GACtB,EAAO,OAAO,CAAO,GACrB,EAAO,MAAM,OAAO,CAAM,GAE1B,EAAO,YACL,MAAM,KAAK,KAAK,QAAQ,CAAC,CAAC,QAAO,MAAS,EAAM,SAAS,SAAS,GAClE,CACF,GACO;CACT;CAEA,sBAA8B;EAU5B,AARA,KAAK,UAAU,KAAA,GACf,KAAK,iBAAiB,KAAA,GACtB,KAAK,OAAO,YAAY,KAAK,OAAO,GAMpC,KAAK,kBAAkB;CACzB;CAEA,MAAc,gBAAgB;EAG5B,AAAI,MAFiB,KAAK,OAAO,YAAY,KAAK,OAAO,MAGvD,KAAK,UAAU,KAAA,GACf,KAAK,iBAAiB,KAAA;CAE1B;CAEA,SAAkB;EAGhB,IAAM,IAAY,GAAG,KAAK,UAAU,WAC9B,IACJ,CAAC,KAAK,UAAU,KAAK,WAAW,KAAK,gBAAgB,IAChD,KAAK,SAAS,KAAK,aAAa,EAAE,MAAM,IACzC;EAEN,OAAO,CAAI;;;;iCAIkB,KAAK,gBAAgB;mBACnC,KAAK,aAAa;qBAChB,KAAK,cAAc,OAAO,IAAI;;0BAEzB,KAAK,QAAQ;;0BAEb,KAAK,SAAS,IAAU,EAAU;0BAClC,OAAO,KAAK,WAAW,EAAE;kCACjB,EAAiB;;6CAEN,KAAK,qBAAqB;YAC1D,KAAK,kBAA4E,IAA1D,CAAI,uBAAuB,KAAK,eAAe,SAAmB;6CACzD,EAAkB;;4FAE6B,EAAU;;;kCAGpE,KAAK,cAAc;;;;;;CAMnD;AAOF;AApfG,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,GACzC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,UAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;CAAM,WAAW;AAAiB,CAAC,CAAA,GAAA,EAAA,WAAA,gBAAA,KAAA,CAAA,GACtE,EAAA,CAAA,EAAS,EAAE,WAAW,GAAM,CAAC,CAAA,GAAA,EAAA,WAAA,oBAAA,KAAA,CAAA,GAC7B,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GAIxC,EAAA,CAAA,EAAM,CAAA,GAAA,EAAA,WAAA,UAAA,KAAA,CAAA,GACN,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,SAAA,IAAA,GAwCxC,EAAA,CAAA,EAAM,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GACN,EAAA,CAAA,EAAM,CAAA,GAAA,EAAA,WAAA,gBAAA,KAAA,CAAA,GACN,EAAA,CAAA,EAAM,CAAA,GAAA,EAAA,WAAA,kBAAA,KAAA,CAAA,GACN,EAAA,CAAA,EAAM,CAAA,GAAA,EAAA,WAAA,mBAAA,KAAA,CAAA,GA1DR,IAAA,IAAA,EAAA,CAAA,EAAc,eAAe,CAAA,GAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/switch/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\nimport { styleMap } from 'lit/directives/style-map.js'\n\nimport '@/components/icon'\nimport glass from '@/assets/glass.css?inline'\nimport { lucideLoaderCircle } from '@/icons'\nimport { installPointerFocusSuppression } from '@/shared/focus/pointer-focus'\nimport { FormAssociated, defineFormAssociation, FormAssociationController } from '@/shared/form-association'\nimport { createFieldId } from '@/shared/form-association/field-id'\nimport { attachDragGesture, type DragGestureHandle } from '@/shared/gesture/drag-gesture'\nimport { clamp, normalizeProgress } from '@/shared/gesture/physics'\n\nimport style from './style.css?inline'\n\ninstallPointerFocusSuppression()\n\n@customElement('web-ui-switch')\nexport class WebUiSwitch extends FormAssociated(LitElement) {\n static override styles = [unsafeCSS(glass), unsafeCSS(style)]\n @state() private _checked = false\n\n get checked(): boolean {\n return this._checked\n }\n\n set checked(v: boolean) {\n const old = this._checked\n this._checked = v\n this._formAssociation.sync()\n this.requestUpdate('checked', old)\n }\n\n @property({ type: String, reflect: true }) name = ''\n @property({ type: String }) value = ''\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true }) required = false\n @property({ type: Boolean, reflect: true }) loading = false\n @state() private _pressed = false\n @state() private _isDragging = false\n @state() private _dragOffset = 0\n @state() private _dragProgress = 0\n\n private _dragged = false\n private _maxTravel = 20\n private _startOffset = 0\n private _dragGestureHandle: DragGestureHandle | null = null\n private readonly _fieldId = createFieldId('web-ui-switch')\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._dragGestureHandle?.destroy()\n this._dragGestureHandle = null\n }\n\n private get _isDisabled(): boolean {\n return this.disabled || this._formAssociation.isFormDisabled()\n }\n\n private readonly _formAssociation = defineFormAssociation<boolean>({\n host: this,\n initialize: () => {\n if (this.hasAttribute('checked')) this._checked = true\n },\n getState: () => this._checked,\n setState: checked => {\n this.checked = checked\n },\n getFormValue: () => (this._checked ? this.value || 'on' : null),\n getFormState: () => String(this._checked),\n restoreState: state => {\n if (typeof state === 'string') this.checked = state === 'true'\n },\n syncValidity: () => this._syncValidity()\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n private _syncValidity() {\n const internals = this._formAssociation.getInternals()\n if (!internals || typeof internals.setValidity !== 'function') return\n if (this._isDisabled || !this.required || this._checked) internals.setValidity({})\n else internals.setValidity({ valueMissing: true }, '请启用此项')\n }\n\n // 阻止 label 默认行为,避免原生 checkbox 重复触发点击。\n private handleClick(e: Event) {\n e.preventDefault()\n if (this._dragged) {\n this._dragged = false\n return\n }\n if (this._isDisabled || this.loading) return\n const old = this._checked\n this._checked = !old\n this._formAssociation.sync()\n this.requestUpdate('checked', old)\n this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n\n private handlePointerDown(e: PointerEvent) {\n if (this._isDisabled || this.loading) return\n this._pressed = true\n\n const track = this.renderRoot.querySelector<HTMLElement>('.wui-switch-track')\n if (track) {\n const trackWidth = track.getBoundingClientRect().width || 40\n // 拖拽时 thumb 的 pressed 布局盒为 20px(scale(1.5) 视觉放大),因此行程按 20px 布局盒钳制;\n // 释放后的 checked 静止位移为 20px(16px thumb),两者由 transition 衔接。\n this._maxTravel = Math.max(1, trackWidth - 4 - 20)\n } else {\n this._maxTravel = 16\n }\n\n this._startOffset = this._checked ? this._maxTravel : 0\n this._dragOffset = this._startOffset\n this._dragProgress = this._checked ? 1 : 0\n\n this._dragGestureHandle?.destroy()\n this._dragGestureHandle = attachDragGesture(e, {\n axis: 'x',\n threshold: 6,\n onMove: info => {\n this._isDragging = true\n const current = clamp(this._startOffset + info.deltaX, 0, this._maxTravel)\n this._dragOffset = current\n this._dragProgress = normalizeProgress(current, 0, this._maxTravel)\n this.requestUpdate()\n },\n onEnd: info => {\n const wasDragging = this._isDragging\n this._isDragging = false\n this._pressed = false\n\n if (wasDragging) {\n this._dragged = true\n let targetChecked = this._checked\n if (info.velocityX > 300) {\n targetChecked = true\n } else if (info.velocityX < -300) {\n targetChecked = false\n } else {\n targetChecked = this._dragProgress >= 0.5\n }\n\n if (targetChecked !== this._checked) {\n const old = this._checked\n this._checked = targetChecked\n this._formAssociation.sync()\n this.requestUpdate('checked', old)\n this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n } else {\n this.requestUpdate()\n }\n }\n },\n onCancel: () => {\n this._isDragging = false\n this._pressed = false\n this.requestUpdate()\n }\n })\n }\n\n private handlePointerUp() {\n this._pressed = false\n }\n\n private handlePointerLeave() {\n if (!this._isDragging) {\n this._pressed = false\n }\n }\n\n override render() {\n const isOpen = this._isDragging ? this._dragProgress >= 0.5 : this._checked\n const trackCls = {\n 'wui-switch-track': true,\n 'is-open': isOpen,\n 'is-dragging': this._isDragging,\n 'is-disabled': this._isDisabled || this.loading\n }\n // wui-glass 条件化:静止态实体白 thumb,按压/拖拽时切换为玻璃(backdrop blur +\n // 无填充 + 描边)并放大,回到最初修复不跟手问题时期的视觉结构。\n const thumbCls = {\n 'wui-switch-thumb': true,\n 'wui-glass': this._pressed || this._isDragging,\n 'is-pressed': this._pressed || this._isDragging\n }\n const trackStyle = this._isDragging ? { '--wui-switch-drag-offset': `${this._dragOffset}px` } : {}\n\n return html`\n <label\n class=${classMap(trackCls)}\n style=${styleMap(trackStyle)}\n role=\"switch\"\n aria-checked=${String(this._checked)}\n @click=${this.handleClick}\n @pointerdown=${this.handlePointerDown}\n @pointerup=${this.handlePointerUp}\n @pointercancel=${this.handlePointerUp}\n @pointerleave=${this.handlePointerLeave}\n >\n <input\n id=${this._fieldId}\n type=\"checkbox\"\n .checked=${this._checked}\n ?disabled=${this._isDisabled || this.loading}\n class=\"sr-only\"\n />\n <div class=${classMap(thumbCls)}>\n ${\n this.loading\n ? html`<div class=\"wui-switch-loading\">\n <web-ui-icon .icon=${lucideLoaderCircle} size=\"14\" color=\"#08f\" spin></web-ui-icon>\n </div>`\n : ''\n }\n </div>\n </label>\n `\n }\n\n declare readonly $events: {\n input: Event\n change: Event\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-switch': WebUiSwitch\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/switch/index.ts"],"sourcesContent":["import '@/components/icon'\n\nimport { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\nimport { styleMap } from 'lit/directives/style-map.js'\n\nimport glass from '@/assets/glass.css?inline'\nimport { lucideLoaderCircle } from '@/icons'\nimport { installPointerFocusSuppression } from '@/shared/focus/pointer-focus'\nimport { FormAssociated, defineFormAssociation, FormAssociationController } from '@/shared/form-association'\nimport { createFieldId } from '@/shared/form-association/field-id'\nimport { attachDragGesture, type DragGestureHandle } from '@/shared/gesture/drag-gesture'\nimport { clamp, normalizeProgress } from '@/shared/gesture/physics'\n\nimport style from './style.css?inline'\n\ninstallPointerFocusSuppression()\n\n@customElement('web-ui-switch')\nexport class WebUiSwitch extends FormAssociated(LitElement) {\n static override styles = [unsafeCSS(glass), unsafeCSS(style)]\n @state() private _checked = false\n\n get checked(): boolean {\n return this._checked\n }\n\n set checked(v: boolean) {\n const old = this._checked\n this._checked = v\n this._formAssociation.sync()\n this.requestUpdate('checked', old)\n }\n\n @property({ type: String, reflect: true }) name = ''\n @property({ type: String }) value = ''\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true }) required = false\n @property({ type: Boolean, reflect: true }) loading = false\n @state() private _pressed = false\n @state() private _isDragging = false\n @state() private _dragOffset = 0\n @state() private _dragProgress = 0\n\n private _dragged = false\n private _maxTravel = 20\n private _startOffset = 0\n private _dragGestureHandle: DragGestureHandle | null = null\n private readonly _fieldId = createFieldId('web-ui-switch')\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._dragGestureHandle?.destroy()\n this._dragGestureHandle = null\n }\n\n private get _isDisabled(): boolean {\n return this.disabled || this._formAssociation.isFormDisabled()\n }\n\n private readonly _formAssociation = defineFormAssociation<boolean>({\n host: this,\n initialize: () => {\n if (this.hasAttribute('checked')) this._checked = true\n },\n getState: () => this._checked,\n setState: checked => {\n this.checked = checked\n },\n getFormValue: () => (this._checked ? this.value || 'on' : null),\n getFormState: () => String(this._checked),\n restoreState: state => {\n if (typeof state === 'string') this.checked = state === 'true'\n },\n syncValidity: () => this._syncValidity()\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n private _syncValidity() {\n const internals = this._formAssociation.getInternals()\n if (!internals || typeof internals.setValidity !== 'function') return\n if (this._isDisabled || !this.required || this._checked) internals.setValidity({})\n else internals.setValidity({ valueMissing: true }, '请启用此项')\n }\n\n // 阻止 label 默认行为,避免原生 checkbox 重复触发点击。\n private handleClick(e: Event) {\n e.preventDefault()\n if (this._dragged) {\n this._dragged = false\n return\n }\n if (this._isDisabled || this.loading) return\n const old = this._checked\n this._checked = !old\n this._formAssociation.sync()\n this.requestUpdate('checked', old)\n this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n\n private handlePointerDown(e: PointerEvent) {\n if (this._isDisabled || this.loading) return\n this._pressed = true\n\n const track = this.renderRoot.querySelector<HTMLElement>('.wui-switch-track')\n if (track) {\n const trackWidth = track.getBoundingClientRect().width || 40\n // 拖拽时 thumb 的 pressed 布局盒为 20px(scale(1.5) 视觉放大),因此行程按 20px 布局盒钳制;\n // 释放后的 checked 静止位移为 20px(16px thumb),两者由 transition 衔接。\n this._maxTravel = Math.max(1, trackWidth - 4 - 20)\n } else {\n this._maxTravel = 16\n }\n\n this._startOffset = this._checked ? this._maxTravel : 0\n this._dragOffset = this._startOffset\n this._dragProgress = this._checked ? 1 : 0\n\n this._dragGestureHandle?.destroy()\n this._dragGestureHandle = attachDragGesture(e, {\n axis: 'x',\n threshold: 6,\n onMove: info => {\n this._isDragging = true\n const current = clamp(this._startOffset + info.deltaX, 0, this._maxTravel)\n this._dragOffset = current\n this._dragProgress = normalizeProgress(current, 0, this._maxTravel)\n this.requestUpdate()\n },\n onEnd: info => {\n const wasDragging = this._isDragging\n this._isDragging = false\n this._pressed = false\n\n if (wasDragging) {\n this._dragged = true\n let targetChecked = this._checked\n if (info.velocityX > 300) {\n targetChecked = true\n } else if (info.velocityX < -300) {\n targetChecked = false\n } else {\n targetChecked = this._dragProgress >= 0.5\n }\n\n if (targetChecked !== this._checked) {\n const old = this._checked\n this._checked = targetChecked\n this._formAssociation.sync()\n this.requestUpdate('checked', old)\n this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n } else {\n this.requestUpdate()\n }\n }\n },\n onCancel: () => {\n this._isDragging = false\n this._pressed = false\n this.requestUpdate()\n }\n })\n }\n\n private handlePointerUp() {\n this._pressed = false\n }\n\n private handlePointerLeave() {\n if (!this._isDragging) {\n this._pressed = false\n }\n }\n\n override render() {\n const isOpen = this._isDragging ? this._dragProgress >= 0.5 : this._checked\n const trackCls = {\n 'wui-switch-track': true,\n 'is-open': isOpen,\n 'is-dragging': this._isDragging,\n 'is-disabled': this._isDisabled || this.loading\n }\n // wui-glass 条件化:静止态实体白 thumb,按压/拖拽时切换为玻璃(backdrop blur +\n // 无填充 + 描边)并放大,回到最初修复不跟手问题时期的视觉结构。\n const thumbCls = {\n 'wui-switch-thumb': true,\n 'wui-glass': this._pressed || this._isDragging,\n 'is-pressed': this._pressed || this._isDragging\n }\n const trackStyle = this._isDragging ? { '--wui-switch-drag-offset': `${this._dragOffset}px` } : {}\n\n return html`\n <label\n class=${classMap(trackCls)}\n style=${styleMap(trackStyle)}\n role=\"switch\"\n aria-checked=${String(this._checked)}\n @click=${this.handleClick}\n @pointerdown=${this.handlePointerDown}\n @pointerup=${this.handlePointerUp}\n @pointercancel=${this.handlePointerUp}\n @pointerleave=${this.handlePointerLeave}\n >\n <input\n id=${this._fieldId}\n type=\"checkbox\"\n .checked=${this._checked}\n ?disabled=${this._isDisabled || this.loading}\n class=\"sr-only\"\n />\n <div class=${classMap(thumbCls)}>\n ${\n this.loading\n ? html`<div class=\"wui-switch-loading\">\n <web-ui-icon .icon=${lucideLoaderCircle} size=\"14\" color=\"#08f\" spin></web-ui-icon>\n </div>`\n : ''\n }\n </div>\n </label>\n `\n }\n\n declare readonly $events: {\n input: Event\n change: Event\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-switch': WebUiSwitch\n }\n}\n"],"mappings":"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"}
|
|
@@ -8,8 +8,8 @@ import { createFieldId as s } from "../../shared/form-association/field-id.js";
|
|
|
8
8
|
import c from "./style.js";
|
|
9
9
|
import { LitElement as l, html as u, nothing as d, unsafeCSS as f } from "lit";
|
|
10
10
|
import { customElement as p, property as m, state as h } from "lit/decorators.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { classMap as g } from "lit/directives/class-map.js";
|
|
12
|
+
import { ifDefined as _ } from "lit/directives/if-defined.js";
|
|
13
13
|
import { styleMap as v } from "lit/directives/style-map.js";
|
|
14
14
|
//#region src/components/textarea/index.ts
|
|
15
15
|
var y = class extends r(l) {
|
|
@@ -125,16 +125,16 @@ var y = class extends r(l) {
|
|
|
125
125
|
let e = this.clearable && this._value && !this.readonly;
|
|
126
126
|
return u`
|
|
127
127
|
<div class="wui-glass wui-textarea-inner" @click=${this.focusTextarea}>
|
|
128
|
-
<slot name="prefix" class=${
|
|
128
|
+
<slot name="prefix" class=${g({ empty: !this._hasPrefix })} @slotchange=${this._onSlotChange}></slot>
|
|
129
129
|
<textarea
|
|
130
130
|
id=${this._fieldId}
|
|
131
131
|
placeholder=${this.placeholder}
|
|
132
132
|
name=${this.name}
|
|
133
|
-
aria-label=${
|
|
134
|
-
aria-labelledby=${
|
|
133
|
+
aria-label=${_(this.ariaLabel)}
|
|
134
|
+
aria-labelledby=${_(this.ariaLabelledby)}
|
|
135
135
|
.rows=${this.rows}
|
|
136
|
-
minlength=${
|
|
137
|
-
maxlength=${
|
|
136
|
+
minlength=${_(this.minlength)}
|
|
137
|
+
maxlength=${_(this.maxlength)}
|
|
138
138
|
?disabled=${this._isDisabled}
|
|
139
139
|
?readonly=${this.readonly}
|
|
140
140
|
?required=${this.required}
|
|
@@ -155,7 +155,7 @@ var y = class extends r(l) {
|
|
|
155
155
|
>
|
|
156
156
|
<web-ui-icon .icon=${n} size="14"></web-ui-icon>
|
|
157
157
|
</web-ui-button>` : ""}
|
|
158
|
-
<slot name="suffix" class=${
|
|
158
|
+
<slot name="suffix" class=${g({ empty: !this._hasSuffix })} @slotchange=${this._onSlotChange}></slot>
|
|
159
159
|
</div>
|
|
160
160
|
`;
|
|
161
161
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/textarea/index.ts"],"sourcesContent":["import { html, LitElement, nothing, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\nimport { ifDefined } from 'lit/directives/if-defined.js'\nimport { styleMap } from 'lit/directives/style-map.js'\n\nimport '@/components/icon'\nimport '@/components/button'\nimport glass from '@/assets/glass.css?inline'\nimport { jamCloseCircleF } from '@/icons'\nimport {\n FormAssociated,\n defineFormAssociation,\n forwardInputValidity,\n FormAssociationController\n} from '@/shared/form-association'\nimport { createFieldId } from '@/shared/form-association/field-id'\n\nimport style from './style.css?inline'\n\n@customElement('web-ui-textarea')\nexport class WebUiTextarea extends FormAssociated(LitElement) {\n static override styles = [unsafeCSS(glass), unsafeCSS(style)]\n\n @property({ type: String, reflect: true }) placeholder = ''\n @property({ type: String, reflect: true }) name = ''\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true }) readonly = false\n @property({ type: Boolean, reflect: true }) required = false\n @property({ type: Boolean, reflect: true }) full = false\n @property({ type: Boolean, reflect: true }) clearable = false\n @property({ type: Boolean, reflect: true }) borderless = false\n @property({ type: Number, reflect: true }) rows = 3\n @property({ type: Number, reflect: true }) minlength: number | undefined\n @property({ type: Number, reflect: true }) maxlength: number | undefined\n @property({ type: Boolean, reflect: true }) autosize = false\n // 自动高度上限(px);0 表示不限制,随内容无限增高\n @property({ type: Number, reflect: true, attribute: 'max-height' }) maxHeight = 0\n @property({ type: String, attribute: 'aria-label' }) override ariaLabel: string | null = null\n @property({ type: String, attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined\n\n @state() private _value = ''\n @state() private _focused = false\n @state() private _hasPrefix = false\n @state() private _hasSuffix = false\n\n private _textarea: HTMLTextAreaElement | null = null\n private _resizeObserver: ResizeObserver | null = null\n private readonly _fieldId = createFieldId('web-ui-textarea')\n\n @property({ type: String, reflect: true })\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 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 syncValidity: () => this._syncValidity()\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._teardownAutosize()\n }\n\n override firstUpdated() {\n this._textarea = this.shadowRoot?.querySelector('textarea') ?? null\n }\n\n override updated(changed: Map<string, unknown>) {\n if (changed.has('_value')) {\n const textarea = this._getTextarea()\n if (textarea && textarea.value !== this._value) {\n textarea.value = this._value\n }\n }\n /*\n * 高度只在这里算,autosize 的两条改值路径(用户输入、外部改写)都汇到\n * changed.has('_value'),不必各自调一次。\n *\n * 不能等 ResizeObserver:autosize 把高度写成内联 px,换值时盒子尺寸没变,\n * 不会触发回调,于是旧高度会一直留着(例如同一个 textarea 随抽屉切换资源,\n * 从长备注切到短备注仍占着长备注的高度)。\n *\n * rows 同理:它决定 height:auto 时的自然高度,但 inline px 会盖掉它,\n * 改 rows 不会让盒子尺寸变化,ResizeObserver 同样看不到。\n *\n * 上面的 textarea.value 兜底在正常路径上是 no-op——render() 里的 .value 绑定\n * 已经先一步把新值写进 DOM 了;它留给 binding 未生效的场景。\n */\n if (this.autosize && (changed.has('_value') || changed.has('rows'))) this._autosize()\n if (changed.has('autosize')) {\n if (this.autosize) {\n this._setupAutosize()\n } else {\n this._teardownAutosize(true)\n }\n }\n this._syncValidity()\n this.toggleAttribute('focused', this._focused)\n }\n\n private _getTextarea(): HTMLTextAreaElement | null {\n this._textarea ??= this.shadowRoot?.querySelector('textarea') ?? null\n return this._textarea\n }\n\n private _syncValidity() {\n const internals = this._formAssociation.getInternals()\n const textarea = this._getTextarea()\n if (!internals || !textarea || typeof internals.setValidity !== 'function') return\n\n forwardInputValidity(internals, textarea, ['valueMissing', 'tooShort', 'tooLong'], this._isDisabled)\n }\n\n private _setupAutosize() {\n this._teardownAutosize()\n if (typeof ResizeObserver === 'undefined') {\n this._autosize()\n return\n }\n this._resizeObserver = new ResizeObserver(() => {\n this._autosize()\n })\n if (this._textarea) this._resizeObserver.observe(this._textarea)\n this._autosize()\n }\n\n private _teardownAutosize(resetHeight = false) {\n this._resizeObserver?.disconnect()\n this._resizeObserver = null\n if (resetHeight) this._textarea?.style.removeProperty('height')\n }\n\n private _autosize() {\n if (!this._textarea || !this.autosize) return\n this._textarea.style.height = 'auto'\n // height:auto 时 rows 已驱动自然高,scrollHeight 恒 ≥ rows 行高,无需额外 floor;\n // 上限由 CSS max-height(maxHeight 属性)钳制,这里只负责随内容增高,默认无限\n this._textarea.style.height = `${this._textarea.scrollHeight}px`\n }\n\n private handleInput(e: Event) {\n if (this._isDisabled || this.readonly) return\n if (!(e.target instanceof HTMLTextAreaElement)) return\n this._value = e.target.value\n this._formAssociation.sync()\n this._syncValidity()\n }\n\n private handleFocus() {\n if (!this._isDisabled) this._focused = true\n }\n\n private handleBlur() {\n this._focused = false\n }\n\n // 原生 change 不 composed,被 shadow root 挡住;这里补发 composed 事件,\n // 兑现 $events/README 声明的公共 change 契约。\n private handleNativeChange(e: Event) {\n if (this._isDisabled || this.readonly) return\n if (!(e.target instanceof HTMLTextAreaElement)) return\n this._value = e.target.value\n this._formAssociation.sync()\n this._syncValidity()\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n\n override focus(options?: FocusOptions) {\n if (this._isDisabled) return\n this.shadowRoot?.querySelector('textarea')?.focus(options)\n }\n\n override blur() {\n this.shadowRoot?.querySelector('textarea')?.blur()\n }\n\n /**\n * 公共 API:全选当前值。\n *\n * `disabled` 时与 `focus()` 一致不产生效果;原生控件未渲染时安全 no-op。\n */\n select() {\n if (this._isDisabled) return\n this.shadowRoot?.querySelector('textarea')?.select()\n }\n\n private focusTextarea() {\n if (this._isDisabled) return\n this.shadowRoot?.querySelector('textarea')?.focus()\n }\n\n private _onSlotChange(e: Event) {\n if (!(e.target instanceof HTMLSlotElement)) return\n const slot = e.target\n const hasContent = slot.assignedElements().length > 0\n if (slot.name === 'prefix') this._hasPrefix = hasContent\n if (slot.name === 'suffix') this._hasSuffix = hasContent\n }\n\n private handleClear() {\n if (this._isDisabled || this.readonly) return\n this._value = ''\n this._formAssociation.sync()\n this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n }\n\n private preventMouseDownBlur(e: Event) {\n e.preventDefault()\n }\n\n override render() {\n // readonly 下不可清除:清空按钮会修改值,与只读语义冲突\n const showClear = this.clearable && this._value && !this.readonly\n\n return html`\n <div class=\"wui-glass wui-textarea-inner\" @click=${this.focusTextarea}>\n <slot name=\"prefix\" class=${classMap({ empty: !this._hasPrefix })} @slotchange=${this._onSlotChange}></slot>\n <textarea\n id=${this._fieldId}\n placeholder=${this.placeholder}\n name=${this.name}\n aria-label=${ifDefined(this.ariaLabel)}\n aria-labelledby=${ifDefined(this.ariaLabelledby)}\n .rows=${this.rows}\n minlength=${ifDefined(this.minlength)}\n maxlength=${ifDefined(this.maxlength)}\n ?disabled=${this._isDisabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n .value=${this._value}\n style=${this.maxHeight > 0 ? styleMap({ 'max-height': `${this.maxHeight}px` }) : nothing}\n @input=${this.handleInput}\n @change=${this.handleNativeChange}\n @focus=${this.handleFocus}\n @blur=${this.handleBlur}\n ></textarea>\n ${\n showClear\n ? html`<web-ui-button\n icon\n variant=\"ghost\"\n size=\"20\"\n aria-label=\"清除\"\n @pointerdown=${this.preventMouseDownBlur}\n @click=${this.handleClear}\n >\n <web-ui-icon .icon=${jamCloseCircleF} size=\"14\"></web-ui-icon>\n </web-ui-button>`\n : ''\n }\n <slot name=\"suffix\" class=${classMap({ empty: !this._hasSuffix })} @slotchange=${this._onSlotChange}></slot>\n </div>\n `\n }\n\n declare readonly $events: {\n input: Event\n change: Event\n focus: FocusEvent\n blur: FocusEvent\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-textarea': WebUiTextarea\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/textarea/index.ts"],"sourcesContent":["import '@/components/button'\nimport '@/components/icon'\n\nimport { html, LitElement, nothing, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\nimport { ifDefined } from 'lit/directives/if-defined.js'\nimport { styleMap } from 'lit/directives/style-map.js'\n\nimport glass from '@/assets/glass.css?inline'\nimport { jamCloseCircleF } from '@/icons'\nimport {\n FormAssociated,\n defineFormAssociation,\n forwardInputValidity,\n FormAssociationController\n} from '@/shared/form-association'\nimport { createFieldId } from '@/shared/form-association/field-id'\n\nimport style from './style.css?inline'\n\n@customElement('web-ui-textarea')\nexport class WebUiTextarea extends FormAssociated(LitElement) {\n static override styles = [unsafeCSS(glass), unsafeCSS(style)]\n\n @property({ type: String, reflect: true }) placeholder = ''\n @property({ type: String, reflect: true }) name = ''\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true }) readonly = false\n @property({ type: Boolean, reflect: true }) required = false\n @property({ type: Boolean, reflect: true }) full = false\n @property({ type: Boolean, reflect: true }) clearable = false\n @property({ type: Boolean, reflect: true }) borderless = false\n @property({ type: Number, reflect: true }) rows = 3\n @property({ type: Number, reflect: true }) minlength: number | undefined\n @property({ type: Number, reflect: true }) maxlength: number | undefined\n @property({ type: Boolean, reflect: true }) autosize = false\n // 自动高度上限(px);0 表示不限制,随内容无限增高\n @property({ type: Number, reflect: true, attribute: 'max-height' }) maxHeight = 0\n @property({ type: String, attribute: 'aria-label' }) override ariaLabel: string | null = null\n @property({ type: String, attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined\n\n @state() private _value = ''\n @state() private _focused = false\n @state() private _hasPrefix = false\n @state() private _hasSuffix = false\n\n private _textarea: HTMLTextAreaElement | null = null\n private _resizeObserver: ResizeObserver | null = null\n private readonly _fieldId = createFieldId('web-ui-textarea')\n\n @property({ type: String, reflect: true })\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 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 syncValidity: () => this._syncValidity()\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._teardownAutosize()\n }\n\n override firstUpdated() {\n this._textarea = this.shadowRoot?.querySelector('textarea') ?? null\n }\n\n override updated(changed: Map<string, unknown>) {\n if (changed.has('_value')) {\n const textarea = this._getTextarea()\n if (textarea && textarea.value !== this._value) {\n textarea.value = this._value\n }\n }\n /*\n * 高度只在这里算,autosize 的两条改值路径(用户输入、外部改写)都汇到\n * changed.has('_value'),不必各自调一次。\n *\n * 不能等 ResizeObserver:autosize 把高度写成内联 px,换值时盒子尺寸没变,\n * 不会触发回调,于是旧高度会一直留着(例如同一个 textarea 随抽屉切换资源,\n * 从长备注切到短备注仍占着长备注的高度)。\n *\n * rows 同理:它决定 height:auto 时的自然高度,但 inline px 会盖掉它,\n * 改 rows 不会让盒子尺寸变化,ResizeObserver 同样看不到。\n *\n * 上面的 textarea.value 兜底在正常路径上是 no-op——render() 里的 .value 绑定\n * 已经先一步把新值写进 DOM 了;它留给 binding 未生效的场景。\n */\n if (this.autosize && (changed.has('_value') || changed.has('rows'))) this._autosize()\n if (changed.has('autosize')) {\n if (this.autosize) {\n this._setupAutosize()\n } else {\n this._teardownAutosize(true)\n }\n }\n this._syncValidity()\n this.toggleAttribute('focused', this._focused)\n }\n\n private _getTextarea(): HTMLTextAreaElement | null {\n this._textarea ??= this.shadowRoot?.querySelector('textarea') ?? null\n return this._textarea\n }\n\n private _syncValidity() {\n const internals = this._formAssociation.getInternals()\n const textarea = this._getTextarea()\n if (!internals || !textarea || typeof internals.setValidity !== 'function') return\n\n forwardInputValidity(internals, textarea, ['valueMissing', 'tooShort', 'tooLong'], this._isDisabled)\n }\n\n private _setupAutosize() {\n this._teardownAutosize()\n if (typeof ResizeObserver === 'undefined') {\n this._autosize()\n return\n }\n this._resizeObserver = new ResizeObserver(() => {\n this._autosize()\n })\n if (this._textarea) this._resizeObserver.observe(this._textarea)\n this._autosize()\n }\n\n private _teardownAutosize(resetHeight = false) {\n this._resizeObserver?.disconnect()\n this._resizeObserver = null\n if (resetHeight) this._textarea?.style.removeProperty('height')\n }\n\n private _autosize() {\n if (!this._textarea || !this.autosize) return\n this._textarea.style.height = 'auto'\n // height:auto 时 rows 已驱动自然高,scrollHeight 恒 ≥ rows 行高,无需额外 floor;\n // 上限由 CSS max-height(maxHeight 属性)钳制,这里只负责随内容增高,默认无限\n this._textarea.style.height = `${this._textarea.scrollHeight}px`\n }\n\n private handleInput(e: Event) {\n if (this._isDisabled || this.readonly) return\n if (!(e.target instanceof HTMLTextAreaElement)) return\n this._value = e.target.value\n this._formAssociation.sync()\n this._syncValidity()\n }\n\n private handleFocus() {\n if (!this._isDisabled) this._focused = true\n }\n\n private handleBlur() {\n this._focused = false\n }\n\n // 原生 change 不 composed,被 shadow root 挡住;这里补发 composed 事件,\n // 兑现 $events/README 声明的公共 change 契约。\n private handleNativeChange(e: Event) {\n if (this._isDisabled || this.readonly) return\n if (!(e.target instanceof HTMLTextAreaElement)) return\n this._value = e.target.value\n this._formAssociation.sync()\n this._syncValidity()\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n\n override focus(options?: FocusOptions) {\n if (this._isDisabled) return\n this.shadowRoot?.querySelector('textarea')?.focus(options)\n }\n\n override blur() {\n this.shadowRoot?.querySelector('textarea')?.blur()\n }\n\n /**\n * 公共 API:全选当前值。\n *\n * `disabled` 时与 `focus()` 一致不产生效果;原生控件未渲染时安全 no-op。\n */\n select() {\n if (this._isDisabled) return\n this.shadowRoot?.querySelector('textarea')?.select()\n }\n\n private focusTextarea() {\n if (this._isDisabled) return\n this.shadowRoot?.querySelector('textarea')?.focus()\n }\n\n private _onSlotChange(e: Event) {\n if (!(e.target instanceof HTMLSlotElement)) return\n const slot = e.target\n const hasContent = slot.assignedElements().length > 0\n if (slot.name === 'prefix') this._hasPrefix = hasContent\n if (slot.name === 'suffix') this._hasSuffix = hasContent\n }\n\n private handleClear() {\n if (this._isDisabled || this.readonly) return\n this._value = ''\n this._formAssociation.sync()\n this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n }\n\n private preventMouseDownBlur(e: Event) {\n e.preventDefault()\n }\n\n override render() {\n // readonly 下不可清除:清空按钮会修改值,与只读语义冲突\n const showClear = this.clearable && this._value && !this.readonly\n\n return html`\n <div class=\"wui-glass wui-textarea-inner\" @click=${this.focusTextarea}>\n <slot name=\"prefix\" class=${classMap({ empty: !this._hasPrefix })} @slotchange=${this._onSlotChange}></slot>\n <textarea\n id=${this._fieldId}\n placeholder=${this.placeholder}\n name=${this.name}\n aria-label=${ifDefined(this.ariaLabel)}\n aria-labelledby=${ifDefined(this.ariaLabelledby)}\n .rows=${this.rows}\n minlength=${ifDefined(this.minlength)}\n maxlength=${ifDefined(this.maxlength)}\n ?disabled=${this._isDisabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n .value=${this._value}\n style=${this.maxHeight > 0 ? styleMap({ 'max-height': `${this.maxHeight}px` }) : nothing}\n @input=${this.handleInput}\n @change=${this.handleNativeChange}\n @focus=${this.handleFocus}\n @blur=${this.handleBlur}\n ></textarea>\n ${\n showClear\n ? html`<web-ui-button\n icon\n variant=\"ghost\"\n size=\"20\"\n aria-label=\"清除\"\n @pointerdown=${this.preventMouseDownBlur}\n @click=${this.handleClear}\n >\n <web-ui-icon .icon=${jamCloseCircleF} size=\"14\"></web-ui-icon>\n </web-ui-button>`\n : ''\n }\n <slot name=\"suffix\" class=${classMap({ empty: !this._hasSuffix })} @slotchange=${this._onSlotChange}></slot>\n </div>\n `\n }\n\n declare readonly $events: {\n input: Event\n change: Event\n focus: FocusEvent\n blur: FocusEvent\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-textarea': WebUiTextarea\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toast.js","names":[],"sources":["../../../src/components/toast/toast.ts"],"sourcesContent":["import type { IconifyIcon } from '@iconify/types'\nimport { html, LitElement, nothing, type PropertyValues, unsafeCSS } from 'lit'\n\nimport '@/components/icon'\n// web-ui-button 复用 icon 变体的焦点环与统一样式(secondary 变体,浅灰底贴合 toast 角落)\nimport '@/components/button'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport glass from '@/assets/glass.css?inline'\nimport {\n lucideInfo,\n lucideTriangleAlert,\n lucideCircleAlert,\n heroiconsXMark16Solid,\n heroiconsCheck16Solid\n} from '@/icons'\nimport { getTransitionDuration } from '@/shared/overlay/presence'\n\nimport style from './style.css?inline'\nimport type { ToastCloseReason, ToastPosition, ToastType } from './types'\n\nconst TYPE_ICONS: Record<ToastType, IconifyIcon> = {\n success: heroiconsCheck16Solid,\n info: lucideInfo,\n warning: lucideTriangleAlert,\n error: lucideCircleAlert\n}\n\nfunction _formatTime(): string {\n const now = new Date()\n const h = now.getHours().toString().padStart(2, '0')\n const m = now.getMinutes().toString().padStart(2, '0')\n return `${h}:${m}`\n}\n\n@customElement('web-ui-toast')\nexport class WebUiToast extends LitElement {\n static override styles = [unsafeCSS(glass), unsafeCSS(style)]\n\n @property({ type: String, attribute: false }) toastId = ''\n @property({ type: String, reflect: true }) type: ToastType = 'info'\n @property({ type: String, reflect: true }) position: ToastPosition = 'top-right'\n @property({ type: String }) heading = ''\n @property({ type: String }) message = ''\n @property({ type: Number }) duration = 3000\n @property({ type: Boolean, reflect: true, attribute: 'no-close-button' }) noCloseButton = false\n @property({ type: Boolean, reflect: true }) visible = false\n\n private _closeTimer?: ReturnType<typeof setTimeout>\n private _deadline = 0\n /**\n * 暂停自动关闭时记下的剩余时间。`undefined` 表示暂停时本来就没有计时器(`duration` 为 0、\n * 或元素尚未 `show()`),与「已到期」(剩余 0)是两件事,不能共用一个 0。\n */\n private _pausedRemaining?: number\n /** 指针停在 toast 上:计时处于悬停暂停态,只有指针离开才能续跑。 */\n private _hoverPaused = false\n private _dismissing = false\n\n /**\n * 退场已开始、`toast-close` 尚未派发。这段窗口内元素仍连接在 DOM、也仍被 manager 的映射\n * 持有,但已不再接受属性更新:manager 的 upsert 据此走新建路径,否则「更新」会写进一条\n * 正在消失的元素里 —— 调用方拿到同一个 id,却看不到任何可见通知。\n */\n get dismissing(): boolean {\n return this._dismissing\n }\n\n /** 指针仍停在 toast 上,自动关闭计时处于暂停态。 */\n get hoverPaused(): boolean {\n return this._hoverPaused\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this.addEventListener('pointerenter', this._onPointerEnter)\n this.addEventListener('pointerleave', this._onPointerLeave)\n // 悬停中被搬迁(不支持 moveBefore 的引擎走摘出重插)时暂停态要连兜底一起带回来,\n // 否则新位置上既没有 document 监听、也不会再补发 pointerenter,这条 toast 又变成永久停留。\n if (this._hoverPaused) this._attachHoverFallback()\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this.removeEventListener('pointerenter', this._onPointerEnter)\n this.removeEventListener('pointerleave', this._onPointerLeave)\n this._detachHoverFallback()\n this._clearTimer()\n }\n\n /**\n * 定义该回调后,`Element.moveBefore()` 搬迁节点不再触发 disconnected/connected,\n * 自动关闭计时与动画状态得以保留;未定义时旧引擎会走摘出重插,计时被 `_clearTimer()` 清掉。\n */\n connectedMoveCallback() {}\n\n protected override updated(changed: PropertyValues) {\n if (changed.has('position')) {\n this._applySlideDirection()\n }\n }\n\n private _applySlideDirection() {\n const SLIDE_OFFSET = 20\n let x = 0\n let y = 0\n\n if (this.position.includes('left')) x = -SLIDE_OFFSET\n else if (this.position.includes('right')) x = SLIDE_OFFSET\n\n if (this.position.startsWith('top')) y = -SLIDE_OFFSET\n else if (this.position.startsWith('bottom')) y = SLIDE_OFFSET\n\n this.style.setProperty('--toast-slide-x', `${x}px`)\n this.style.setProperty('--toast-slide-y', `${y}px`)\n }\n\n startAutoClose() {\n this._clearTimer()\n if (this.duration > 0) {\n this._deadline = Date.now() + this.duration\n this._closeTimer = setTimeout(() => {\n this.dismiss('auto')\n }, this.duration)\n }\n }\n\n /**\n * 由 manager 调用:显式 `duration` 更新。悬停暂停期间只记新值、不点火 —— 否则指针还压在\n * toast 上时计时就被重启,「悬停暂停」在这条路径上直接失效。恢复时按新 duration 计满:\n * 调用方显式改了生命周期,续跑按旧时长算出的剩余没有意义。`0` 表示不自动关闭,记成\n * 「没有计时器」,否则恢复时会被当成「已到期」立即退场。\n */\n setDuration(ms: number) {\n this.duration = ms\n if (this._hoverPaused) {\n this._pausedRemaining = ms > 0 ? ms : undefined\n return\n }\n this.startAutoClose()\n }\n\n // 不支持 moveBefore 的引擎上搬迁节点前调用:记下剩余时间,搬完由 resumeAutoClose() 续跑。\n pauseAutoClose() {\n // 已经在暂停态时保留既有剩余:悬停暂停期间若发生搬迁,降级路径也会走到这里,无条件\n // 重算会把悬停记下的剩余抹成「没有计时器」,指针离开后这条 toast 就再也不会关闭。\n if (this._closeTimer === undefined && this._pausedRemaining !== undefined) return\n // 计时器仍在跑时剩余时间可能算出 0:deadline 已过、回调还排在队列里没执行。这一支与\n // 「本来就没有计时器」必须分开记(undefined vs 0),否则续跑无法区分两者。\n this._pausedRemaining = this._closeTimer === undefined ? undefined : Math.max(0, this._deadline - Date.now())\n this._clearTimer()\n }\n\n // 与 pauseAutoClose() 成对:暂停时没有计时器就保持原状,不重启满时长;\n // 已到期则立即退场 —— 而不是把「已到期」当成「从来没有计时器」放过,那样这条 toast 会永不关闭。\n resumeAutoClose() {\n // 悬停还没结束时只交还剩余时间、不点火:搬迁降级路径也会走到这里,真正的续跑等指针\n // 离开(_resumeFromHover)再开始,否则鼠标还停着就已经在倒计时了。\n if (this._hoverPaused) return\n const remaining = this._pausedRemaining\n this._pausedRemaining = undefined\n if (remaining === undefined) return\n // 装新计时器前先拆掉可能仍在跑的旧计时器:pause/resume 一旦不再严格成对(调用序被重排),\n // 直接覆盖 `_closeTimer` 会留下两个 deadline 同时倒计时,先到的那个把 toast 提前关掉。\n this._clearTimer()\n if (remaining <= 0) {\n this.dismiss('auto')\n return\n }\n this._deadline = Date.now() + remaining\n this._closeTimer = setTimeout(() => {\n this.dismiss('auto')\n }, remaining)\n }\n\n // 由 manager 调用:播放入场动画后自动开始计时\n show() {\n this._dismissing = false\n this.visible = true\n // 挂载时指针已经压在上面(光标停在 toast 将要出现的位置):入场后直接进入暂停,\n // 不能点火 —— 那条计时会在指针还停着的时候把 toast 关掉。\n if (this._hoverPaused) {\n this._pausedRemaining = this.duration > 0 ? this.duration : undefined\n return\n }\n this.startAutoClose()\n }\n\n // 含退场动画,结束由 transitionend 驱动后派发 close。\n dismiss(reason: ToastCloseReason = 'programmatic') {\n if (this._dismissing) return\n this._dismissing = true\n // 退场优先于悬停:暂停态不再有意义,document 兜底监听也要收掉,否则退场中的\n // 元素会继续持有「指针还在上面」的状态。\n this._hoverPaused = false\n this._detachHoverFallback()\n if (!this.visible) {\n // 尚未 show()(manager 挂载之后、rAF 之前)或从未显示过:没有退场动画可播,立即派发,\n // 由 manager 的 toast-close 监听摘除元素。若在这里早退,close()/clear() 落在这段窗口\n // 里就会静默失效 —— 元素照常入场,调用方却以为已经关掉了。\n this._dispatchClose(reason)\n return\n }\n this.visible = false\n this._clearTimer()\n const onEnd = () => {\n this.removeEventListener('transitionend', onEnd)\n this._dispatchClose(reason)\n }\n this.addEventListener('transitionend', onEnd)\n // fallback: transitionend 不触发时(display:none 等)\n const toast = this.shadowRoot?.querySelector<HTMLElement>('.toast')\n setTimeout(onEnd, getTransitionDuration(toast ?? this) + 80)\n }\n\n private _clearTimer() {\n if (this._closeTimer !== undefined) {\n clearTimeout(this._closeTimer)\n this._closeTimer = undefined\n }\n }\n\n private _onPointerEnter = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (this._hoverPaused) return\n this._hoverPaused = true\n this.pauseAutoClose()\n // pointerleave 是唯一的恢复入口时,漏掉一次(指针拖出窗口、元素在悬停期间被搬迁或\n // 摘出、光标静止时布局把 toast 移走)这条 toast 就永久停在屏幕上。document 级兜底\n // 补上这个缺口:指针落到别的元素上、或离开文档,都判定悬停结束。三个信号互相兜底,\n // 因为单一信号在不同引擎上都有漏派发的场景。\n this._attachHoverFallback()\n }\n\n private _onPointerLeave = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n this._resumeFromHover()\n }\n\n // 兜底路径:指针移到别的元素上(冒泡到 document)。路径不含本元素说明指针已经不在\n // toast 上;composedPath() 覆盖 shadow DOM,指针在 toast 内部节点之间移动不算离开。\n private _onDocumentPointerOver = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (e.composedPath().includes(this)) return\n this._resumeFromHover()\n }\n\n // 兜底路径:指针移出窗口。部分引擎不保证 document 上的 pointerleave,用 relatedTarget\n // 为空的 pointerout 补上 —— 悬停中的 toast 被摘除时同样会派发这种 pointerout。\n private _onDocumentPointerOut = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (e.relatedTarget !== null) return\n this._resumeFromHover()\n }\n\n // 兜底路径:指针离开整个文档(移出窗口)。文档内的元素间移动不会派发到 document。\n private _onDocumentPointerLeave = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n this._resumeFromHover()\n }\n\n private _resumeFromHover() {\n if (!this._hoverPaused) return\n this._hoverPaused = false\n this._detachHoverFallback()\n this.resumeAutoClose()\n }\n\n private _attachHoverFallback() {\n document.addEventListener('pointerover', this._onDocumentPointerOver)\n document.addEventListener('pointerout', this._onDocumentPointerOut)\n document.addEventListener('pointerleave', this._onDocumentPointerLeave)\n }\n\n private _detachHoverFallback() {\n document.removeEventListener('pointerover', this._onDocumentPointerOver)\n document.removeEventListener('pointerout', this._onDocumentPointerOut)\n document.removeEventListener('pointerleave', this._onDocumentPointerLeave)\n }\n\n private _onCloseClick = () => {\n this.dismiss('manual')\n }\n\n private _dispatchClose(reason: ToastCloseReason) {\n this.dispatchEvent(\n new CustomEvent('toast-close', {\n detail: { id: this.toastId, reason },\n bubbles: true,\n composed: true\n })\n )\n }\n\n override render() {\n const icon = TYPE_ICONS[this.type]\n const ariaLive = this.type === 'error' ? 'assertive' : 'polite'\n // role=\"alert\" 会强制隐式 aria-live=\"assertive\",覆盖掉上方 aria-live 设置,\n // 因此仅 error toast 使用 alert(本就应 assertive);其余类型交给容器 role=\"log\" 的 polite 播报。\n const role = this.type === 'error' ? 'alert' : nothing\n\n return html`\n <div class=\"toast wui-glass ${this.type}\" role=${role} aria-live=${ariaLive} aria-atomic=\"true\">\n <span class=\"toast-icon\" aria-hidden=\"true\">\n <web-ui-icon .icon=${icon} size=\"14\"></web-ui-icon>\n </span>\n <div class=\"toast-body\">\n ${this.heading ? html`<div class=\"toast-heading\">${this.heading}</div>` : nothing}\n <div class=\"toast-message\">${this.message}</div>\n </div>\n <span class=\"toast-time\">${_formatTime()}</span>\n ${\n !this.noCloseButton\n ? html`\n <web-ui-button\n class=\"toast-close-btn\"\n icon\n variant=\"secondary\"\n size=\"24\"\n aria-label=\"关闭\"\n @click=${this._onCloseClick}\n >\n <web-ui-icon .icon=${heroiconsXMark16Solid} size=\"16\"></web-ui-icon>\n </web-ui-button>\n `\n : nothing\n }\n </div>\n `\n }\n\n declare readonly $events: {\n 'toast-close': CustomEvent<{ id: string; reason: ToastCloseReason }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-toast': WebUiToast\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"toast.js","names":[],"sources":["../../../src/components/toast/toast.ts"],"sourcesContent":["// web-ui-button 复用 icon 变体的焦点环与统一样式(secondary 变体,浅灰底贴合 toast 角落)\nimport '@/components/button'\nimport '@/components/icon'\n\nimport type { IconifyIcon } from '@iconify/types'\nimport { html, LitElement, nothing, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport glass from '@/assets/glass.css?inline'\nimport {\n lucideInfo,\n lucideTriangleAlert,\n lucideCircleAlert,\n heroiconsXMark16Solid,\n heroiconsCheck16Solid\n} from '@/icons'\nimport { getTransitionDuration } from '@/shared/overlay/presence'\n\nimport style from './style.css?inline'\nimport type { ToastCloseReason, ToastPosition, ToastType } from './types'\n\nconst TYPE_ICONS: Record<ToastType, IconifyIcon> = {\n success: heroiconsCheck16Solid,\n info: lucideInfo,\n warning: lucideTriangleAlert,\n error: lucideCircleAlert\n}\n\nfunction _formatTime(): string {\n const now = new Date()\n const h = now.getHours().toString().padStart(2, '0')\n const m = now.getMinutes().toString().padStart(2, '0')\n return `${h}:${m}`\n}\n\n@customElement('web-ui-toast')\nexport class WebUiToast extends LitElement {\n static override styles = [unsafeCSS(glass), unsafeCSS(style)]\n\n @property({ type: String, attribute: false }) toastId = ''\n @property({ type: String, reflect: true }) type: ToastType = 'info'\n @property({ type: String, reflect: true }) position: ToastPosition = 'top-right'\n @property({ type: String }) heading = ''\n @property({ type: String }) message = ''\n @property({ type: Number }) duration = 3000\n @property({ type: Boolean, reflect: true, attribute: 'no-close-button' }) noCloseButton = false\n @property({ type: Boolean, reflect: true }) visible = false\n\n private _closeTimer?: ReturnType<typeof setTimeout>\n private _deadline = 0\n /**\n * 暂停自动关闭时记下的剩余时间。`undefined` 表示暂停时本来就没有计时器(`duration` 为 0、\n * 或元素尚未 `show()`),与「已到期」(剩余 0)是两件事,不能共用一个 0。\n */\n private _pausedRemaining?: number\n /** 指针停在 toast 上:计时处于悬停暂停态,只有指针离开才能续跑。 */\n private _hoverPaused = false\n private _dismissing = false\n\n /**\n * 退场已开始、`toast-close` 尚未派发。这段窗口内元素仍连接在 DOM、也仍被 manager 的映射\n * 持有,但已不再接受属性更新:manager 的 upsert 据此走新建路径,否则「更新」会写进一条\n * 正在消失的元素里 —— 调用方拿到同一个 id,却看不到任何可见通知。\n */\n get dismissing(): boolean {\n return this._dismissing\n }\n\n /** 指针仍停在 toast 上,自动关闭计时处于暂停态。 */\n get hoverPaused(): boolean {\n return this._hoverPaused\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this.addEventListener('pointerenter', this._onPointerEnter)\n this.addEventListener('pointerleave', this._onPointerLeave)\n // 悬停中被搬迁(不支持 moveBefore 的引擎走摘出重插)时暂停态要连兜底一起带回来,\n // 否则新位置上既没有 document 监听、也不会再补发 pointerenter,这条 toast 又变成永久停留。\n if (this._hoverPaused) this._attachHoverFallback()\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this.removeEventListener('pointerenter', this._onPointerEnter)\n this.removeEventListener('pointerleave', this._onPointerLeave)\n this._detachHoverFallback()\n this._clearTimer()\n }\n\n /**\n * 定义该回调后,`Element.moveBefore()` 搬迁节点不再触发 disconnected/connected,\n * 自动关闭计时与动画状态得以保留;未定义时旧引擎会走摘出重插,计时被 `_clearTimer()` 清掉。\n */\n connectedMoveCallback() {}\n\n protected override updated(changed: PropertyValues) {\n if (changed.has('position')) {\n this._applySlideDirection()\n }\n }\n\n private _applySlideDirection() {\n const SLIDE_OFFSET = 20\n let x = 0\n let y = 0\n\n if (this.position.includes('left')) x = -SLIDE_OFFSET\n else if (this.position.includes('right')) x = SLIDE_OFFSET\n\n if (this.position.startsWith('top')) y = -SLIDE_OFFSET\n else if (this.position.startsWith('bottom')) y = SLIDE_OFFSET\n\n this.style.setProperty('--toast-slide-x', `${x}px`)\n this.style.setProperty('--toast-slide-y', `${y}px`)\n }\n\n startAutoClose() {\n this._clearTimer()\n if (this.duration > 0) {\n this._deadline = Date.now() + this.duration\n this._closeTimer = setTimeout(() => {\n this.dismiss('auto')\n }, this.duration)\n }\n }\n\n /**\n * 由 manager 调用:显式 `duration` 更新。悬停暂停期间只记新值、不点火 —— 否则指针还压在\n * toast 上时计时就被重启,「悬停暂停」在这条路径上直接失效。恢复时按新 duration 计满:\n * 调用方显式改了生命周期,续跑按旧时长算出的剩余没有意义。`0` 表示不自动关闭,记成\n * 「没有计时器」,否则恢复时会被当成「已到期」立即退场。\n */\n setDuration(ms: number) {\n this.duration = ms\n if (this._hoverPaused) {\n this._pausedRemaining = ms > 0 ? ms : undefined\n return\n }\n this.startAutoClose()\n }\n\n // 不支持 moveBefore 的引擎上搬迁节点前调用:记下剩余时间,搬完由 resumeAutoClose() 续跑。\n pauseAutoClose() {\n // 已经在暂停态时保留既有剩余:悬停暂停期间若发生搬迁,降级路径也会走到这里,无条件\n // 重算会把悬停记下的剩余抹成「没有计时器」,指针离开后这条 toast 就再也不会关闭。\n if (this._closeTimer === undefined && this._pausedRemaining !== undefined) return\n // 计时器仍在跑时剩余时间可能算出 0:deadline 已过、回调还排在队列里没执行。这一支与\n // 「本来就没有计时器」必须分开记(undefined vs 0),否则续跑无法区分两者。\n this._pausedRemaining = this._closeTimer === undefined ? undefined : Math.max(0, this._deadline - Date.now())\n this._clearTimer()\n }\n\n // 与 pauseAutoClose() 成对:暂停时没有计时器就保持原状,不重启满时长;\n // 已到期则立即退场 —— 而不是把「已到期」当成「从来没有计时器」放过,那样这条 toast 会永不关闭。\n resumeAutoClose() {\n // 悬停还没结束时只交还剩余时间、不点火:搬迁降级路径也会走到这里,真正的续跑等指针\n // 离开(_resumeFromHover)再开始,否则鼠标还停着就已经在倒计时了。\n if (this._hoverPaused) return\n const remaining = this._pausedRemaining\n this._pausedRemaining = undefined\n if (remaining === undefined) return\n // 装新计时器前先拆掉可能仍在跑的旧计时器:pause/resume 一旦不再严格成对(调用序被重排),\n // 直接覆盖 `_closeTimer` 会留下两个 deadline 同时倒计时,先到的那个把 toast 提前关掉。\n this._clearTimer()\n if (remaining <= 0) {\n this.dismiss('auto')\n return\n }\n this._deadline = Date.now() + remaining\n this._closeTimer = setTimeout(() => {\n this.dismiss('auto')\n }, remaining)\n }\n\n // 由 manager 调用:播放入场动画后自动开始计时\n show() {\n this._dismissing = false\n this.visible = true\n // 挂载时指针已经压在上面(光标停在 toast 将要出现的位置):入场后直接进入暂停,\n // 不能点火 —— 那条计时会在指针还停着的时候把 toast 关掉。\n if (this._hoverPaused) {\n this._pausedRemaining = this.duration > 0 ? this.duration : undefined\n return\n }\n this.startAutoClose()\n }\n\n // 含退场动画,结束由 transitionend 驱动后派发 close。\n dismiss(reason: ToastCloseReason = 'programmatic') {\n if (this._dismissing) return\n this._dismissing = true\n // 退场优先于悬停:暂停态不再有意义,document 兜底监听也要收掉,否则退场中的\n // 元素会继续持有「指针还在上面」的状态。\n this._hoverPaused = false\n this._detachHoverFallback()\n if (!this.visible) {\n // 尚未 show()(manager 挂载之后、rAF 之前)或从未显示过:没有退场动画可播,立即派发,\n // 由 manager 的 toast-close 监听摘除元素。若在这里早退,close()/clear() 落在这段窗口\n // 里就会静默失效 —— 元素照常入场,调用方却以为已经关掉了。\n this._dispatchClose(reason)\n return\n }\n this.visible = false\n this._clearTimer()\n const onEnd = () => {\n this.removeEventListener('transitionend', onEnd)\n this._dispatchClose(reason)\n }\n this.addEventListener('transitionend', onEnd)\n // fallback: transitionend 不触发时(display:none 等)\n const toast = this.shadowRoot?.querySelector<HTMLElement>('.toast')\n setTimeout(onEnd, getTransitionDuration(toast ?? this) + 80)\n }\n\n private _clearTimer() {\n if (this._closeTimer !== undefined) {\n clearTimeout(this._closeTimer)\n this._closeTimer = undefined\n }\n }\n\n private _onPointerEnter = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (this._hoverPaused) return\n this._hoverPaused = true\n this.pauseAutoClose()\n // pointerleave 是唯一的恢复入口时,漏掉一次(指针拖出窗口、元素在悬停期间被搬迁或\n // 摘出、光标静止时布局把 toast 移走)这条 toast 就永久停在屏幕上。document 级兜底\n // 补上这个缺口:指针落到别的元素上、或离开文档,都判定悬停结束。三个信号互相兜底,\n // 因为单一信号在不同引擎上都有漏派发的场景。\n this._attachHoverFallback()\n }\n\n private _onPointerLeave = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n this._resumeFromHover()\n }\n\n // 兜底路径:指针移到别的元素上(冒泡到 document)。路径不含本元素说明指针已经不在\n // toast 上;composedPath() 覆盖 shadow DOM,指针在 toast 内部节点之间移动不算离开。\n private _onDocumentPointerOver = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (e.composedPath().includes(this)) return\n this._resumeFromHover()\n }\n\n // 兜底路径:指针移出窗口。部分引擎不保证 document 上的 pointerleave,用 relatedTarget\n // 为空的 pointerout 补上 —— 悬停中的 toast 被摘除时同样会派发这种 pointerout。\n private _onDocumentPointerOut = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (e.relatedTarget !== null) return\n this._resumeFromHover()\n }\n\n // 兜底路径:指针离开整个文档(移出窗口)。文档内的元素间移动不会派发到 document。\n private _onDocumentPointerLeave = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n this._resumeFromHover()\n }\n\n private _resumeFromHover() {\n if (!this._hoverPaused) return\n this._hoverPaused = false\n this._detachHoverFallback()\n this.resumeAutoClose()\n }\n\n private _attachHoverFallback() {\n document.addEventListener('pointerover', this._onDocumentPointerOver)\n document.addEventListener('pointerout', this._onDocumentPointerOut)\n document.addEventListener('pointerleave', this._onDocumentPointerLeave)\n }\n\n private _detachHoverFallback() {\n document.removeEventListener('pointerover', this._onDocumentPointerOver)\n document.removeEventListener('pointerout', this._onDocumentPointerOut)\n document.removeEventListener('pointerleave', this._onDocumentPointerLeave)\n }\n\n private _onCloseClick = () => {\n this.dismiss('manual')\n }\n\n private _dispatchClose(reason: ToastCloseReason) {\n this.dispatchEvent(\n new CustomEvent('toast-close', {\n detail: { id: this.toastId, reason },\n bubbles: true,\n composed: true\n })\n )\n }\n\n override render() {\n const icon = TYPE_ICONS[this.type]\n const ariaLive = this.type === 'error' ? 'assertive' : 'polite'\n // role=\"alert\" 会强制隐式 aria-live=\"assertive\",覆盖掉上方 aria-live 设置,\n // 因此仅 error toast 使用 alert(本就应 assertive);其余类型交给容器 role=\"log\" 的 polite 播报。\n const role = this.type === 'error' ? 'alert' : nothing\n\n return html`\n <div class=\"toast wui-glass ${this.type}\" role=${role} aria-live=${ariaLive} aria-atomic=\"true\">\n <span class=\"toast-icon\" aria-hidden=\"true\">\n <web-ui-icon .icon=${icon} size=\"14\"></web-ui-icon>\n </span>\n <div class=\"toast-body\">\n ${this.heading ? html`<div class=\"toast-heading\">${this.heading}</div>` : nothing}\n <div class=\"toast-message\">${this.message}</div>\n </div>\n <span class=\"toast-time\">${_formatTime()}</span>\n ${\n !this.noCloseButton\n ? html`\n <web-ui-button\n class=\"toast-close-btn\"\n icon\n variant=\"secondary\"\n size=\"24\"\n aria-label=\"关闭\"\n @click=${this._onCloseClick}\n >\n <web-ui-icon .icon=${heroiconsXMark16Solid} size=\"16\"></web-ui-icon>\n </web-ui-button>\n `\n : nothing\n }\n </div>\n `\n }\n\n declare readonly $events: {\n 'toast-close': CustomEvent<{ id: string; reason: ToastCloseReason }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-toast': WebUiToast\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/scrollbars/index.ts"],"sourcesContent":["import { ClickScrollPlugin, OverlayScrollbars, type PartialOptions } from 'overlayscrollbars'\n\
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/scrollbars/index.ts"],"sourcesContent":["import './style.css'\n\nimport { ClickScrollPlugin, OverlayScrollbars, type PartialOptions } from 'overlayscrollbars'\n\n/**\n * `scrollbars.theme` 的取值,同时也是 `@greypan/web-ui/scrollbars.css` 里主题类的名字。\n */\nexport const WEB_UI_SCROLLBARS_THEME = 'os-theme-macos'\n\n/*\n * track 点击滚动由 ClickScrollPlugin 提供;插件未注册时 `clickScroll` 静默失效——\n * 不报错、不打日志,轨道只是没有反应。注册写在模块作用域,只要消费方用了本模块,\n * 插件就一定在位,不会因为漏掉一行 bootstrap 而退化。\n */\nOverlayScrollbars.plugin(ClickScrollPlugin)\n\n/**\n * 全仓统一的滚动条行为:外观对齐 macOS、自动隐藏、滚动时短暂显示、track 点击直达。\n *\n * 内容尺寸或平台变化都不需要改这里,只有三条产品要求本身变了才改。覆盖层要实现的是\n * 同一套手势语言,所以用法是把它整个传给 OverlayScrollbars 初始化,而不是逐项拼接:\n *\n * ```ts\n * import { webUiScrollbarsOptions } from '@greypan/web-ui/scrollbars'\n * import '@greypan/web-ui/scrollbars.css'\n * ```\n *\n * - `autoHide: 'scroll'`:滚动条只在滚动时出现,停下 `autoHideDelay` 之后自动隐去。用户明确选了\n * 这一档而不是 `'move'`——`'move'` 会让指针在滚动区域内移动就唤出,比 macOS 原生积极得多。\n * 代价写清楚:**hover 本身不再唤出滚动条**(隐藏态的元素不可命中,指针根本落不到它上面),\n * 所以 issue 里「hover 的时候短暂显示」那条只以「滚动时短暂显示」的形式满足。\n * - `autoHideDelay: 2000`:这一档同时是「track 点击跳转」的可用窗口——滚动条隐去后它就不可命中。\n * 指针从滚动的位置移到轨道、再点下去,实测预算在 1s 上下,默认的 1300ms 太贴边;2000ms 留出\n * 余量,又不至于让半透明滚动条在静止画面上久留(macOS 原生的淡出也在 2s 这个量级)。\n * - `autoHideSuspend` 保持关闭:打开会让滚动条在首次滚动前一直可见,与「自动隐藏」相反。\n * - `clickScroll` 走函数形态:`true` 只是「按一个视口步进」,跳到的是当前位置的下/上一屏,\n * 与点击处的距离无关。`clickScrollDistance: 0` 才是「以点击处为目标」——库把 0 定义为\n * 「距离等于目标距离」。函数形态是必需的:这个选项只在实现里经过 `isFunction` 分支才会被读到,\n * 直接传对象会被静默忽略。\n * - `clickScrollDuration: 0`:库默认用 200ms 缓动滚到点击处,在渲染虚拟列表的资源列表上会卡顿,\n * 这里改成即时落位。0 是合法时长而不是「非法值被忽略」:`animateNumber` 先用 `Math.max(0, ms)`\n * 得到时长 0,首帧(同步执行、不经 rAF)就判定已到期并落到终点。\n */\nexport const webUiScrollbarsOptions: PartialOptions = {\n scrollbars: {\n theme: WEB_UI_SCROLLBARS_THEME,\n autoHide: 'scroll',\n autoHideDelay: 2000,\n clickScroll: () => ({ clickScrollDistance: 0, clickScrollDuration: 0 })\n }\n}\n"],"mappings":";;;AAOA,IAAa,IAA0B;AAOvC,EAAkB,OAAO,CAAiB;AA6B1C,IAAa,IAAyC,EACpD,YAAY;CACV,OAAO;CACP,UAAU;CACV,eAAe;CACf,oBAAoB;EAAE,qBAAqB;EAAG,qBAAqB;CAAE;AACvE,EACF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { defineOverlayPositioningGeneration as e } from "./positioning-generation.js";
|
|
2
|
+
import { definePlugin as t } from "@greypan/js-kit";
|
|
3
|
+
import { autoUpdate as n, computePosition as r, flip as i, offset as a, shift as o, size as s } from "@floating-ui/dom";
|
|
4
4
|
//#region src/shared/overlay/overlay.ts
|
|
5
5
|
var c = 120, l = "--wui-overlay-min-width", u = {
|
|
6
6
|
placement: "bottom-start",
|
|
@@ -20,57 +20,57 @@ function f(e) {
|
|
|
20
20
|
e.style.removeProperty("width"), e.style.removeProperty("min-width");
|
|
21
21
|
}
|
|
22
22
|
function p(e, t, n, r, i) {
|
|
23
|
-
let
|
|
24
|
-
if (t.matchWidth) return e.style.removeProperty("min-width"), n.hasManagedStyles = !0,
|
|
25
|
-
|
|
23
|
+
let a = () => r.isCurrent(i);
|
|
24
|
+
if (t.matchWidth) return e.style.removeProperty("min-width"), n.hasManagedStyles = !0, s({ apply({ rects: t }) {
|
|
25
|
+
a() && (e.style.width = `${t.reference.width}px`);
|
|
26
26
|
} });
|
|
27
27
|
if (t.minAnchorWidth) {
|
|
28
28
|
let t = d(e);
|
|
29
|
-
return n.hasManagedStyles = !0,
|
|
30
|
-
|
|
29
|
+
return n.hasManagedStyles = !0, s({ apply({ rects: n }) {
|
|
30
|
+
a() && (e.style.width = "max-content", e.style.minWidth = `${Math.max(n.reference.width, t)}px`);
|
|
31
31
|
} });
|
|
32
32
|
}
|
|
33
33
|
n.hasManagedStyles &&= (f(e), !1);
|
|
34
34
|
}
|
|
35
|
-
function m(e, t,
|
|
36
|
-
let c = [
|
|
37
|
-
t.flip && c.push(
|
|
38
|
-
let l = p(e, t,
|
|
35
|
+
function m(e, t, n, r, s) {
|
|
36
|
+
let c = [a(t.offset)];
|
|
37
|
+
t.flip && c.push(i()), t.shift && c.push(o({ padding: 8 }));
|
|
38
|
+
let l = p(e, t, n, r, s);
|
|
39
39
|
return l && c.push(l), c;
|
|
40
40
|
}
|
|
41
41
|
function h(e) {
|
|
42
42
|
let [t, n] = e.split("-"), r = n === "start" ? "left" : n === "end" ? "right" : "center", i = n === "start" ? "top" : n === "end" ? "bottom" : "center";
|
|
43
43
|
return t === "top" ? `bottom ${r}` : t === "bottom" ? `top ${r}` : t === "left" ? `right ${i}` : `left ${i}`;
|
|
44
44
|
}
|
|
45
|
-
var g = () =>
|
|
46
|
-
let
|
|
47
|
-
placement:
|
|
48
|
-
offset:
|
|
49
|
-
flip:
|
|
50
|
-
shift:
|
|
51
|
-
matchWidth:
|
|
52
|
-
minAnchorWidth:
|
|
53
|
-
strategy:
|
|
54
|
-
},
|
|
45
|
+
var g = () => t((t) => {
|
|
46
|
+
let i = {
|
|
47
|
+
placement: t.placement ?? u.placement,
|
|
48
|
+
offset: t.offset ?? u.offset,
|
|
49
|
+
flip: t.flip ?? u.flip,
|
|
50
|
+
shift: t.shift ?? u.shift,
|
|
51
|
+
matchWidth: t.matchWidth ?? u.matchWidth,
|
|
52
|
+
minAnchorWidth: t.minAnchorWidth ?? u.minAnchorWidth,
|
|
53
|
+
strategy: t.strategy ?? u.strategy
|
|
54
|
+
}, a = t.overlay, o = { hasManagedStyles: !1 }, s = !1, c = null, l = t.anchor, d = e().make();
|
|
55
55
|
function f() {
|
|
56
|
-
let
|
|
57
|
-
|
|
58
|
-
placement:
|
|
59
|
-
strategy:
|
|
60
|
-
middleware:
|
|
61
|
-
}).then(({ x:
|
|
62
|
-
d.isCurrent(
|
|
56
|
+
let e = d.next(), t = m(a, i, o, d, e), u = () => {
|
|
57
|
+
r(l, a, {
|
|
58
|
+
placement: i.placement,
|
|
59
|
+
strategy: i.strategy,
|
|
60
|
+
middleware: t
|
|
61
|
+
}).then(({ x: t, y: n, placement: r }) => {
|
|
62
|
+
d.isCurrent(e) && s && (a.style.left = `${t}px`, a.style.top = `${n}px`, a.style.setProperty("--wui-internal-overlay-transform-origin", h(r)));
|
|
63
63
|
});
|
|
64
64
|
};
|
|
65
|
-
u(), c?.(), c =
|
|
65
|
+
u(), c?.(), c = n(l, a, u);
|
|
66
66
|
}
|
|
67
67
|
return {
|
|
68
68
|
isOpen() {
|
|
69
69
|
return s;
|
|
70
70
|
},
|
|
71
|
-
options:
|
|
71
|
+
options: i,
|
|
72
72
|
open() {
|
|
73
|
-
s || (s = !0,
|
|
73
|
+
s || (s = !0, a.style.display = "", f());
|
|
74
74
|
},
|
|
75
75
|
close() {
|
|
76
76
|
s && (s = !1, d.invalidate(), c?.(), c = null);
|
|
@@ -79,7 +79,7 @@ var g = () => s((n) => {
|
|
|
79
79
|
s ? this.close() : this.open();
|
|
80
80
|
},
|
|
81
81
|
update(e) {
|
|
82
|
-
Object.assign(
|
|
82
|
+
Object.assign(i, e), s && f();
|
|
83
83
|
},
|
|
84
84
|
updateAnchor(e) {
|
|
85
85
|
l = e, s && f();
|