@zeyos/zx 5.0.0 → 5.2.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/CHANGELOG.md +164 -0
- package/README.md +2 -1
- package/dist/types/components/account-menu/account-menu.d.ts +8 -0
- package/dist/types/components/activity-list/activity-list.d.ts +9 -1
- package/dist/types/components/app-icon/app-icon.d.ts +14 -2
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +288 -14
- package/dist/types/components/calendar/calendar.d.ts +67 -5
- package/dist/types/components/card-view/card-view.d.ts +7 -4
- package/dist/types/components/card-view/record-card.d.ts +11 -0
- package/dist/types/components/context-menu/context-menu.d.ts +6 -2
- package/dist/types/components/data-filter/data-filter.d.ts +4 -4
- package/dist/types/components/dropdown/dropdown.d.ts +22 -7
- package/dist/types/components/field/field.d.ts +30 -0
- package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
- package/dist/types/components/fieldset/fieldset.d.ts +7 -0
- package/dist/types/components/file-list/file-list.d.ts +35 -3
- package/dist/types/components/filter/filter.d.ts +127 -3
- package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
- package/dist/types/components/launcher/launcher.d.ts +8 -0
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +15 -6
- package/dist/types/components/menu-button/menu-button.d.ts +6 -0
- package/dist/types/components/message/message.d.ts +14 -1
- package/dist/types/components/modal/modal.d.ts +30 -0
- package/dist/types/components/module-bar/module-bar.d.ts +395 -0
- package/dist/types/components/pagination/pagination.d.ts +17 -3
- package/dist/types/components/select/select.d.ts +6 -0
- package/dist/types/components/sheet/sheet.d.ts +15 -0
- package/dist/types/components/table/table.d.ts +265 -17
- package/dist/types/components/view/record-view.d.ts +59 -2
- package/dist/types/components/view-settings/view-settings.d.ts +879 -0
- package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
- package/dist/types/core/component.d.ts +12 -2
- package/dist/types/core/i18n.d.ts +27 -3
- package/dist/types/core/keyboard.d.ts +98 -1
- package/dist/types/core/messages.d.ts +382 -0
- package/dist/types/core/overlay-host.d.ts +28 -1
- package/dist/types/core/overlay-stack.d.ts +260 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/internal/app-rail.d.ts +38 -0
- package/dist/types/internal/menu-items.d.ts +71 -3
- package/dist/types/zeyos/index.d.ts +1 -0
- package/dist/types/zeyos/launcher.d.ts +9 -0
- package/dist/types/zeyos/module-bar.d.ts +176 -0
- package/dist/types/zeyos/query.d.ts +31 -5
- package/dist/types/zeyos/table.d.ts +245 -8
- package/dist/types/zeyos/view.d.ts +16 -2
- package/dist/zx-zeyos.esm.js +4422 -430
- package/dist/zx-zeyos.esm.min.js +5 -5
- package/dist/zx.css +1417 -134
- package/dist/zx.esm.js +6986 -830
- package/dist/zx.esm.min.js +8 -8
- package/dist/zx.global.js +6986 -830
- package/dist/zx.global.min.js +8 -8
- package/dist/zx.min.css +1 -1
- package/docs/api.json +794 -17
- package/docs/llms.md +503 -63
- package/docs/llms.txt +6 -2
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.js +12 -0
- package/src/components/activity-list/activity-list.js +27 -1
- package/src/components/app-icon/app-icon.css +13 -2
- package/src/components/app-icon/app-icon.js +11 -1
- package/src/components/app-rail/app-rail.css +19 -0
- package/src/components/app-sidebar/app-sidebar.css +204 -3
- package/src/components/app-sidebar/app-sidebar.js +646 -40
- package/src/components/breadcrumb/breadcrumb.css +1 -1
- package/src/components/calendar/calendar.css +1 -1
- package/src/components/calendar/calendar.js +26 -52
- package/src/components/card/card.css +2 -2
- package/src/components/card-view/card-view.css +5 -5
- package/src/components/card-view/card-view.js +13 -9
- package/src/components/card-view/record-card.js +14 -3
- package/src/components/code-editor/code-editor.css +1 -1
- package/src/components/code-editor/field-adapter.js +9 -2
- package/src/components/context-menu/context-menu.js +8 -2
- package/src/components/copy/copy-input.css +1 -1
- package/src/components/data-filter/data-filter.js +2 -2
- package/src/components/date-picker/time-picker.css +2 -2
- package/src/components/dock/dock.css +5 -4
- package/src/components/dropdown/dropdown.js +157 -72
- package/src/components/field/field.js +33 -3
- package/src/components/fieldset/external-adapter.js +363 -0
- package/src/components/fieldset/fieldset.js +10 -59
- package/src/components/file-list/file-list.js +144 -21
- package/src/components/filter/filter.css +86 -2
- package/src/components/filter/filter.js +300 -8
- package/src/components/filter-panel/filter-panel.js +149 -32
- package/src/components/finder/finder.css +1 -1
- package/src/components/kanban-view/kanban-view.css +3 -3
- package/src/components/kanban-view/kanban-view.js +5 -2
- package/src/components/launcher/launcher.css +1 -1
- package/src/components/launcher/launcher.js +29 -3
- package/src/components/list-toolbar/list-toolbar.js +39 -13
- package/src/components/master-panel/master-panel.js +0 -1
- package/src/components/menu-button/menu-button.css +63 -0
- package/src/components/menu-button/menu-button.js +97 -11
- package/src/components/message/message.js +60 -20
- package/src/components/modal/modal.css +1 -1
- package/src/components/modal/modal.js +66 -13
- package/src/components/module-bar/module-bar.css +486 -0
- package/src/components/module-bar/module-bar.js +1074 -0
- package/src/components/multi-value-editor/multi-value-editor.css +1 -1
- package/src/components/number-field/field-adapter.js +2 -1
- package/src/components/pagination/pagination.css +5 -5
- package/src/components/pagination/pagination.js +20 -4
- package/src/components/panel/panel.css +3 -2
- package/src/components/questionnaire/questionnaire.css +4 -4
- package/src/components/rating/field-adapter.js +2 -1
- package/src/components/rating/rating.css +1 -1
- package/src/components/search/search.css +4 -1
- package/src/components/select/select.js +9 -0
- package/src/components/sheet/sheet.css +1 -1
- package/src/components/sheet/sheet.js +74 -32
- package/src/components/slider/field-adapter.js +6 -1
- package/src/components/slider/slider.css +2 -2
- package/src/components/split-view/split-view.css +1 -1
- package/src/components/stat-tile/stat-tile.css +1 -1
- package/src/components/stepper/stepper.css +1 -1
- package/src/components/tabbox/tabbox.css +9 -5
- package/src/components/table/table.css +86 -11
- package/src/components/table/table.js +628 -57
- package/src/components/tag-picker/field-adapter.js +2 -1
- package/src/components/tooltip/tooltip.js +35 -17
- package/src/components/tree/tree.css +1 -1
- package/src/components/view/record-view.css +1 -1
- package/src/components/view/record-view.js +178 -18
- package/src/components/view-settings/view-settings.css +285 -0
- package/src/components/view-settings/view-settings.js +1830 -0
- package/src/components/workspace-switcher/workspace-switcher.css +188 -0
- package/src/components/workspace-switcher/workspace-switcher.js +463 -0
- package/src/core/component.js +88 -17
- package/src/core/fontawesome.js +5 -1
- package/src/core/i18n.js +47 -4
- package/src/core/icons.js +12 -2
- package/src/core/keyboard.js +206 -11
- package/src/core/messages.js +427 -0
- package/src/core/overlay-host.js +64 -9
- package/src/core/overlay-stack.js +487 -0
- package/src/core/position.js +22 -3
- package/src/index.js +5 -2
- package/src/internal/app-rail.js +134 -53
- package/src/internal/menu-items.js +61 -3
- package/src/zeyos/form.js +5 -3
- package/src/zeyos/index.js +3 -0
- package/src/zeyos/launcher.js +8 -3
- package/src/zeyos/module-bar.js +181 -0
- package/src/zeyos/query.js +44 -13
- package/src/zeyos/table.js +220 -80
- package/src/zeyos/view.js +43 -93
- package/styles/base.css +16 -1
- package/styles/print.css +1 -1
- package/styles/tokens/dark.css +4 -0
- package/styles/tokens/semantic.css +56 -28
- package/styles/zx.css +3 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The overlay kernel: one z-order, one registry of open overlays, and one document-level
|
|
5
|
+
* dispatcher for Escape, outside presses, and focus leaving an overlay.
|
|
6
|
+
*
|
|
7
|
+
* Before it, every overlay counted its own order and listened to the document itself, and none of
|
|
8
|
+
* them knew about the others: a non-modal Sheet closed itself when an item was chosen in a menu it
|
|
9
|
+
* had opened, and one Escape closed a sheet while leaving its dropdown stranded on a hidden anchor.
|
|
10
|
+
* Here each open overlay is one entry, and two rules decide everything:
|
|
11
|
+
*
|
|
12
|
+
* - **Escape goes to one layer.** The topmost entry that handles Escape gets it, and only that
|
|
13
|
+
* one. An event that arrives `defaultPrevented` was already spent by something inside (a Select
|
|
14
|
+
* closing its own list) and reaches no overlay at all. A modal `<dialog>` handles Escape natively,
|
|
15
|
+
* so reaching one stops the search without touching the event.
|
|
16
|
+
* - **Inside means inside the opener chain.** Each entry records the overlay its anchor sits in as
|
|
17
|
+
* its parent. A press inside a popup anchored from within a sheet is therefore inside the sheet,
|
|
18
|
+
* wherever in the DOM the popup's panel lives; a press outside both dismisses both.
|
|
19
|
+
*
|
|
20
|
+
* Closing an entry also closes the entries opened from it that asked to follow their parent, so a
|
|
21
|
+
* sheet never leaves a dropdown open on an element that is no longer on screen.
|
|
22
|
+
*
|
|
23
|
+
* The document listeners exist only while at least one overlay is open, and go away with the last
|
|
24
|
+
* one through a single AbortController.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What kind of layer an entry is.
|
|
29
|
+
*
|
|
30
|
+
* - `modal` — a `showModal()` dialog. Escape and the backdrop belong to the browser, and nothing
|
|
31
|
+
* below a modal reacts to presses or keys.
|
|
32
|
+
* - `non-modal` — a dialog that leaves the page live (a floating non-modal Sheet).
|
|
33
|
+
* - `popup` — an anchored panel: a Dropdown, a menu, a flyout.
|
|
34
|
+
* - `tooltip` — a description bubble. Any press outside it dismisses it, whatever lies above, and
|
|
35
|
+
* it is never anybody's parent.
|
|
36
|
+
* - `toast` — a message region. It never takes Escape and is never a parent, and a press inside one
|
|
37
|
+
* dismisses nothing, so acting on a toast does not close the drawer it reports on.
|
|
38
|
+
* @typedef {'modal'|'non-modal'|'popup'|'tooltip'|'toast'} OverlayKind
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} OverlayEntryOptions
|
|
43
|
+
* @property {OverlayKind} kind Layer kind.
|
|
44
|
+
* @property {Element} root The overlay's own root: the panel, dialog, or region.
|
|
45
|
+
* @property {unknown} [owner=null] Component that owns the entry; informational.
|
|
46
|
+
* @property {Element|null|Array<Element|null|undefined>} [anchor=null] Element(s) the overlay was
|
|
47
|
+
* opened from or is anchored to. The topmost entry whose root contains one becomes the parent.
|
|
48
|
+
* @property {Array<Element|null|undefined>} [inside=[]] Further elements that count as part of the
|
|
49
|
+
* overlay for outside presses and focus — a dropdown's trigger, which toggles it on its own.
|
|
50
|
+
* @property {(event: KeyboardEvent, handle: OverlayHandle) => boolean|void} [onEscape] Handles an
|
|
51
|
+
* Escape routed to this entry. Return `false` to decline, which passes it to the next layer down.
|
|
52
|
+
* The dispatcher calls `preventDefault()` on an Escape an entry handled.
|
|
53
|
+
* @property {(event: Event, handle: OverlayHandle) => void} [onOutsidePointer] Called for a press
|
|
54
|
+
* outside the entry's chain, while nothing above it contained the press.
|
|
55
|
+
* @property {(event: FocusEvent, handle: OverlayHandle) => void} [onFocusOutside] Called when focus
|
|
56
|
+
* moves to an element outside the entry's chain.
|
|
57
|
+
* @property {(handle: OverlayHandle) => void} [onParentClose] Called when the parent entry closes.
|
|
58
|
+
* An entry without it outlives its parent and is re-parented to the grandparent.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* One registered overlay, as the stack keeps it.
|
|
63
|
+
* @typedef {Object} OverlayEntry
|
|
64
|
+
* @property {number} order Z-order; higher is on top. Unique and increasing, never reused.
|
|
65
|
+
* @property {OverlayKind} kind Layer kind.
|
|
66
|
+
* @property {Element} root Overlay root.
|
|
67
|
+
* @property {unknown} owner Owning component.
|
|
68
|
+
* @property {Element[]} anchors Anchor elements.
|
|
69
|
+
* @property {Element[]} inside Extra elements that count as inside.
|
|
70
|
+
* @property {OverlayEntry|null} parent Overlay the anchor sat in when the entry opened.
|
|
71
|
+
* @property {OverlayEntryOptions['onEscape']} [onEscape] Escape handler.
|
|
72
|
+
* @property {OverlayEntryOptions['onOutsidePointer']} [onOutsidePointer] Outside-press handler.
|
|
73
|
+
* @property {OverlayEntryOptions['onFocusOutside']} [onFocusOutside] Focus-outside handler.
|
|
74
|
+
* @property {OverlayEntryOptions['onParentClose']} [onParentClose] Parent-close handler.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returned by `push()`: the owner's grip on its entry.
|
|
79
|
+
* @typedef {Object} OverlayHandle
|
|
80
|
+
* @property {OverlayEntry} entry The registered entry.
|
|
81
|
+
* @property {number} order The entry's z-order, for `data-zx-overlay-order`.
|
|
82
|
+
* @property {() => boolean} isOpen Whether the entry is still registered.
|
|
83
|
+
* @property {() => boolean} isTopmost Whether no other entry (toasts aside) sits above it.
|
|
84
|
+
* @property {(node: Node|null|undefined) => boolean} contains Whether a node is inside the entry's
|
|
85
|
+
* chain: its root, its `inside` elements, or any overlay opened from it.
|
|
86
|
+
* @property {() => void} release Unregisters the entry and closes the children that follow it.
|
|
87
|
+
* Safe to call repeatedly.
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
/** Kinds that can never contain another overlay's anchor. */
|
|
91
|
+
const LEAF_KINDS = new Set(['tooltip', 'toast']);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether `node` is `root` or inside it, crossing shadow roots.
|
|
95
|
+
* @param {Node|null|undefined} root Container.
|
|
96
|
+
* @param {Node|null|undefined} node Candidate.
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
export function containsNode(root, node) {
|
|
100
|
+
if (!root || !node) return false;
|
|
101
|
+
for (let current = /** @type {any} */ (node); current; current = current.parentNode ?? current.host ?? null) {
|
|
102
|
+
if (current === root) return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The entry an overlay opened from `anchors` belongs under: the topmost one whose root contains an
|
|
109
|
+
* anchor. Tooltips and toasts are never parents.
|
|
110
|
+
* @param {readonly OverlayEntry[]} entries Registered entries, bottom to top.
|
|
111
|
+
* @param {readonly Element[]} anchors Anchor elements of the overlay being opened.
|
|
112
|
+
* @returns {OverlayEntry|null}
|
|
113
|
+
*/
|
|
114
|
+
export function findParent(entries, anchors) {
|
|
115
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
116
|
+
const entry = entries[index];
|
|
117
|
+
if (LEAF_KINDS.has(entry.kind)) continue;
|
|
118
|
+
if (anchors.some((anchor) => containsNode(entry.root, anchor))) return entry;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Whether a node is inside an entry's chain: its root, its `inside` elements, or — recursively —
|
|
125
|
+
* any entry whose parent it is.
|
|
126
|
+
* @param {readonly OverlayEntry[]} entries Registered entries.
|
|
127
|
+
* @param {OverlayEntry} entry Entry to test.
|
|
128
|
+
* @param {Node|null|undefined} node Candidate.
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
export function insideChain(entries, entry, node) {
|
|
132
|
+
if (!node) return false;
|
|
133
|
+
if (containsNode(entry.root, node)) return true;
|
|
134
|
+
if (entry.inside.some((element) => containsNode(element, node))) return true;
|
|
135
|
+
return entries.some((candidate) => candidate.parent === entry && insideChain(entries, candidate, node));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The topmost entry, toasts aside, optionally narrowed by a predicate.
|
|
140
|
+
* @param {readonly OverlayEntry[]} entries Registered entries, bottom to top.
|
|
141
|
+
* @param {(entry: OverlayEntry) => boolean} [filter] Extra condition.
|
|
142
|
+
* @returns {OverlayEntry|null}
|
|
143
|
+
*/
|
|
144
|
+
export function topmostEntry(entries, filter = () => true) {
|
|
145
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
146
|
+
const entry = entries[index];
|
|
147
|
+
if (entry.kind !== 'toast' && filter(entry)) return entry;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The entries an Escape may be offered to, in the order they are asked: from the top down, those
|
|
154
|
+
* with an Escape handler, stopping at the first modal (whose Escape is the browser's).
|
|
155
|
+
*
|
|
156
|
+
* `within` is the open modal dialog the key was pressed in, when there is one. Everything outside
|
|
157
|
+
* it is inert, so an entry whose root lies outside it is never offered the key — which keeps a
|
|
158
|
+
* modal the stack does not know about (a `<dialog>` an application opened with `showModal()`)
|
|
159
|
+
* from losing its Escape to a popup left open behind it.
|
|
160
|
+
* @param {readonly OverlayEntry[]} entries Registered entries, bottom to top.
|
|
161
|
+
* @param {Node|null} [within=null] Open modal dialog containing the key's target.
|
|
162
|
+
* @returns {OverlayEntry[]}
|
|
163
|
+
*/
|
|
164
|
+
export function escapeCandidates(entries, within = null) {
|
|
165
|
+
const candidates = [];
|
|
166
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
167
|
+
const entry = entries[index];
|
|
168
|
+
if (entry.kind === 'modal') break;
|
|
169
|
+
if (within && !containsNode(within, entry.root)) continue;
|
|
170
|
+
if (typeof entry.onEscape === 'function') candidates.push(entry);
|
|
171
|
+
}
|
|
172
|
+
return candidates;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The entries a press on `node` dismisses, topmost first.
|
|
177
|
+
*
|
|
178
|
+
* A press inside a toast dismisses nothing. Tooltips go on any press outside them. For the rest,
|
|
179
|
+
* the walk runs from the top down and stops at the first entry whose chain contains the press — it
|
|
180
|
+
* landed on that layer, and the layers beneath are covered by it or own it — or at a modal, below
|
|
181
|
+
* which everything is inert. Every entry passed on the way that handles outside presses is
|
|
182
|
+
* dismissed. As for Escape, a press inside an open modal dialog dismisses nothing outside it.
|
|
183
|
+
* @param {readonly OverlayEntry[]} entries Registered entries, bottom to top.
|
|
184
|
+
* @param {Node|null|undefined} node Press target.
|
|
185
|
+
* @param {Node|null} [within=null] Open modal dialog containing the press.
|
|
186
|
+
* @returns {OverlayEntry[]}
|
|
187
|
+
*/
|
|
188
|
+
export function outsideTargets(entries, node, within = null) {
|
|
189
|
+
if (entries.some((entry) => entry.kind === 'toast' && containsNode(entry.root, node))) return [];
|
|
190
|
+
const targets = [];
|
|
191
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
192
|
+
const entry = entries[index];
|
|
193
|
+
if (entry.kind === 'tooltip') {
|
|
194
|
+
if (!insideChain(entries, entry, node) && entry.onOutsidePointer) targets.push(entry);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
198
|
+
const entry = entries[index];
|
|
199
|
+
if (LEAF_KINDS.has(entry.kind)) continue;
|
|
200
|
+
if (insideChain(entries, entry, node)) break;
|
|
201
|
+
if (entry.kind === 'modal') break;
|
|
202
|
+
if (within && !containsNode(within, entry.root)) continue;
|
|
203
|
+
if (entry.onOutsidePointer) targets.push(entry);
|
|
204
|
+
}
|
|
205
|
+
return targets;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A registry of open overlays. The library uses the one shared instance, `overlayStack`; separate
|
|
210
|
+
* instances exist for tests and for documents other than the global one.
|
|
211
|
+
*/
|
|
212
|
+
export class OverlayStack {
|
|
213
|
+
/** @type {OverlayEntry[]} Bottom to top. */
|
|
214
|
+
#entries = [];
|
|
215
|
+
#order = 0;
|
|
216
|
+
/** @type {AbortController|null} */
|
|
217
|
+
#listening = null;
|
|
218
|
+
/** @type {Set<(entries: readonly OverlayEntry[]) => void>} */
|
|
219
|
+
#listeners = new Set();
|
|
220
|
+
/** @type {EventTarget|null} */
|
|
221
|
+
#target;
|
|
222
|
+
/** @type {WeakMap<OverlayEntry, OverlayHandle>} */
|
|
223
|
+
#handles = new WeakMap();
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {{target?: EventTarget|null}} [options={}] Where to listen; the global document when
|
|
227
|
+
* omitted. Pass `null` to install no listeners and drive the dispatch methods directly.
|
|
228
|
+
*/
|
|
229
|
+
constructor(options = {}) {
|
|
230
|
+
this.#target = options.target === undefined
|
|
231
|
+
? (typeof document === 'undefined' ? null : document)
|
|
232
|
+
: options.target;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Registers an open overlay above every other.
|
|
237
|
+
* @param {OverlayEntryOptions} options Entry description.
|
|
238
|
+
* @returns {OverlayHandle}
|
|
239
|
+
*/
|
|
240
|
+
push(options) {
|
|
241
|
+
const anchors = toList(options.anchor);
|
|
242
|
+
/** @type {OverlayEntry} */
|
|
243
|
+
const entry = {
|
|
244
|
+
order: ++this.#order,
|
|
245
|
+
kind: options.kind,
|
|
246
|
+
root: options.root,
|
|
247
|
+
owner: options.owner ?? null,
|
|
248
|
+
anchors,
|
|
249
|
+
inside: toList(options.inside),
|
|
250
|
+
parent: options.kind === 'toast' ? null : findParent(this.#entries, anchors),
|
|
251
|
+
onEscape: options.onEscape,
|
|
252
|
+
onOutsidePointer: options.onOutsidePointer,
|
|
253
|
+
onFocusOutside: options.onFocusOutside,
|
|
254
|
+
onParentClose: options.onParentClose
|
|
255
|
+
};
|
|
256
|
+
this.#entries.push(entry);
|
|
257
|
+
/** @type {OverlayHandle} */
|
|
258
|
+
const handle = {
|
|
259
|
+
entry,
|
|
260
|
+
order: entry.order,
|
|
261
|
+
isOpen: () => this.#entries.includes(entry),
|
|
262
|
+
isTopmost: () => topmostEntry(this.#entries) === entry,
|
|
263
|
+
contains: (node) => this.#entries.includes(entry) && insideChain(this.#entries, entry, node),
|
|
264
|
+
release: () => this.#release(entry)
|
|
265
|
+
};
|
|
266
|
+
this.#handles.set(entry, handle);
|
|
267
|
+
this.#listen();
|
|
268
|
+
this.#notify();
|
|
269
|
+
return handle;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The registered entries, bottom to top. A copy: changing it changes nothing.
|
|
274
|
+
* @returns {OverlayEntry[]}
|
|
275
|
+
*/
|
|
276
|
+
entries() {
|
|
277
|
+
return this.#entries.slice();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The topmost entry, toasts aside.
|
|
282
|
+
* @param {(entry: OverlayEntry) => boolean} [filter] Extra condition.
|
|
283
|
+
* @returns {OverlayEntry|null}
|
|
284
|
+
*/
|
|
285
|
+
topmost(filter) {
|
|
286
|
+
return topmostEntry(this.#entries, filter);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Root of the topmost open modal dialog, or null when none is open. Asks the registry first and
|
|
291
|
+
* falls back to the document, so a `<dialog>` opened with `showModal()` outside the library still
|
|
292
|
+
* counts.
|
|
293
|
+
* @returns {Element|null}
|
|
294
|
+
*/
|
|
295
|
+
topmostModal() {
|
|
296
|
+
const entry = topmostEntry(this.#entries, (candidate) => candidate.kind === 'modal'
|
|
297
|
+
&& candidate.root.isConnected && matches(candidate.root, ':modal', true));
|
|
298
|
+
if (entry) return entry.root;
|
|
299
|
+
if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return null;
|
|
300
|
+
let modals;
|
|
301
|
+
try {
|
|
302
|
+
modals = document.querySelectorAll('dialog:modal');
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return modals.length ? modals[modals.length - 1] : null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Whether a node is inside any registered overlay's root or `inside` elements.
|
|
311
|
+
* @param {Node|null|undefined} node Candidate.
|
|
312
|
+
* @returns {boolean}
|
|
313
|
+
*/
|
|
314
|
+
containsNode(node) {
|
|
315
|
+
return this.#entries.some((entry) => containsNode(entry.root, node)
|
|
316
|
+
|| entry.inside.some((element) => containsNode(element, node)));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Subscribes to registry changes: called after every push and release.
|
|
321
|
+
* @param {(entries: readonly OverlayEntry[]) => void} listener Change listener.
|
|
322
|
+
* @returns {() => void} Unsubscribes.
|
|
323
|
+
*/
|
|
324
|
+
subscribe(listener) {
|
|
325
|
+
this.#listeners.add(listener);
|
|
326
|
+
return () => { this.#listeners.delete(listener); };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Routes an Escape keydown to the topmost layer that handles it. Ignores other keys and events
|
|
331
|
+
* already `defaultPrevented`. Installed on the document while overlays are open; public so a
|
|
332
|
+
* stack created without a target can be driven directly.
|
|
333
|
+
* @param {KeyboardEvent} event Keydown event.
|
|
334
|
+
* @returns {OverlayEntry|null} The entry that handled it.
|
|
335
|
+
*/
|
|
336
|
+
handleKeydown(event) {
|
|
337
|
+
if (event.key !== 'Escape' || event.defaultPrevented || event.isComposing) return null;
|
|
338
|
+
for (const entry of escapeCandidates(this.#entries, modalAround(eventNode(event)))) {
|
|
339
|
+
const handle = this.#handles.get(entry);
|
|
340
|
+
if (!handle || !this.#entries.includes(entry)) continue;
|
|
341
|
+
if (entry.onEscape?.(event, handle) === false) continue;
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
return entry;
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Dismisses the layers a press landed outside of. Installed on the document, capture phase, while
|
|
350
|
+
* overlays are open; public so a stack created without a target can be driven directly.
|
|
351
|
+
* @param {Event} event Pointerdown event.
|
|
352
|
+
* @returns {OverlayEntry[]} The entries asked to dismiss, topmost first.
|
|
353
|
+
*/
|
|
354
|
+
handlePointerdown(event) {
|
|
355
|
+
const node = eventNode(event);
|
|
356
|
+
const targets = outsideTargets(this.#entries, node, modalAround(node));
|
|
357
|
+
for (const entry of targets) {
|
|
358
|
+
const handle = this.#handles.get(entry);
|
|
359
|
+
if (handle && this.#entries.includes(entry)) entry.onOutsidePointer?.(event, handle);
|
|
360
|
+
}
|
|
361
|
+
return targets;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Tells the entries that watch focus that it moved outside them.
|
|
366
|
+
* @param {FocusEvent} event Focusin event.
|
|
367
|
+
* @returns {void}
|
|
368
|
+
*/
|
|
369
|
+
handleFocusin(event) {
|
|
370
|
+
const node = eventNode(event);
|
|
371
|
+
for (const entry of this.#entries.slice().reverse()) {
|
|
372
|
+
if (typeof entry.onFocusOutside !== 'function' || !this.#entries.includes(entry)) continue;
|
|
373
|
+
if (insideChain(this.#entries, entry, node)) continue;
|
|
374
|
+
const handle = this.#handles.get(entry);
|
|
375
|
+
if (handle) entry.onFocusOutside(event, handle);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** @param {OverlayEntry} entry @returns {void} */
|
|
380
|
+
#release(entry) {
|
|
381
|
+
const index = this.#entries.indexOf(entry);
|
|
382
|
+
if (index < 0) return;
|
|
383
|
+
this.#entries.splice(index, 1);
|
|
384
|
+
const children = this.#entries.filter((candidate) => candidate.parent === entry);
|
|
385
|
+
// Re-parented first, so a child that outlives its parent is still in somebody's chain.
|
|
386
|
+
for (const child of children) child.parent = entry.parent;
|
|
387
|
+
if (this.#entries.length === 0) this.#unlisten();
|
|
388
|
+
this.#notify();
|
|
389
|
+
for (const child of children.reverse()) {
|
|
390
|
+
const handle = this.#handles.get(child);
|
|
391
|
+
if (handle && this.#entries.includes(child)) child.onParentClose?.(handle);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** @returns {void} */
|
|
396
|
+
#listen() {
|
|
397
|
+
if (this.#listening || !this.#target || typeof this.#target.addEventListener !== 'function') return;
|
|
398
|
+
const controller = new AbortController();
|
|
399
|
+
this.#listening = controller;
|
|
400
|
+
const { signal } = controller;
|
|
401
|
+
this.#target.addEventListener('keydown', (event) => this.handleKeydown(/** @type {KeyboardEvent} */ (event)), { signal });
|
|
402
|
+
this.#target.addEventListener('pointerdown', (event) => this.handlePointerdown(event), { capture: true, signal });
|
|
403
|
+
this.#target.addEventListener('focusin', (event) => this.handleFocusin(/** @type {FocusEvent} */ (event)), { signal });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** @returns {void} */
|
|
407
|
+
#unlisten() {
|
|
408
|
+
this.#listening?.abort();
|
|
409
|
+
this.#listening = null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** @returns {void} */
|
|
413
|
+
#notify() {
|
|
414
|
+
const snapshot = this.#entries.slice();
|
|
415
|
+
for (const listener of [...this.#listeners]) {
|
|
416
|
+
try {
|
|
417
|
+
listener(snapshot);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
queueMicrotask(() => { throw error; });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The document's overlay stack, shared by every overlay in the library.
|
|
427
|
+
* @type {OverlayStack}
|
|
428
|
+
*/
|
|
429
|
+
export const overlayStack = new OverlayStack();
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Registers an open overlay on the shared stack. Shorthand for `overlayStack.push(options)`.
|
|
433
|
+
* @param {OverlayEntryOptions} options Entry description.
|
|
434
|
+
* @returns {OverlayHandle}
|
|
435
|
+
*/
|
|
436
|
+
export function pushOverlay(options) {
|
|
437
|
+
return overlayStack.push(options);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @param {Element|null|undefined|Array<Element|null|undefined>} value
|
|
442
|
+
* @returns {Element[]}
|
|
443
|
+
*/
|
|
444
|
+
function toList(value) {
|
|
445
|
+
const list = Array.isArray(value) ? value : [value];
|
|
446
|
+
return /** @type {Element[]} */ (list.filter((item, index) => item != null && list.indexOf(item) === index));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* The innermost open modal dialog around a node, or null.
|
|
451
|
+
* @param {Node|null} node
|
|
452
|
+
* @returns {Element|null}
|
|
453
|
+
*/
|
|
454
|
+
function modalAround(node) {
|
|
455
|
+
for (let current = /** @type {any} */ (node); current; current = current.parentNode ?? current.host ?? null) {
|
|
456
|
+
if (current.localName === 'dialog' && typeof current.matches === 'function') {
|
|
457
|
+
try {
|
|
458
|
+
if (current.matches(':modal')) return current;
|
|
459
|
+
} catch {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** @param {Event} event @returns {Node|null} */
|
|
468
|
+
function eventNode(event) {
|
|
469
|
+
const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
|
|
470
|
+
return /** @type {Node|null} */ (path[0] ?? event.target ?? null);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* `element.matches(selector)`, answering `fallback` where the selector is not supported.
|
|
475
|
+
* @param {Element} element
|
|
476
|
+
* @param {string} selector
|
|
477
|
+
* @param {boolean} fallback
|
|
478
|
+
* @returns {boolean}
|
|
479
|
+
*/
|
|
480
|
+
function matches(element, selector, fallback) {
|
|
481
|
+
if (typeof element.matches !== 'function') return fallback;
|
|
482
|
+
try {
|
|
483
|
+
return element.matches(selector);
|
|
484
|
+
} catch {
|
|
485
|
+
return fallback;
|
|
486
|
+
}
|
|
487
|
+
}
|
package/src/core/position.js
CHANGED
|
@@ -46,9 +46,12 @@ export function position(anchor, floating, options = {}) {
|
|
|
46
46
|
|
|
47
47
|
floating.setAttribute('popover', 'manual');
|
|
48
48
|
const supportsAnchors = typeof CSS !== 'undefined' && CSS.supports?.('anchor-name: --zx-a');
|
|
49
|
+
const anchorName = `--${uid('zx-a')}`;
|
|
49
50
|
if (supportsAnchors) {
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
// One element can anchor several floating elements at once — a tooltip and a menu on the same
|
|
52
|
+
// trigger — so the name joins the anchor's list rather than replacing it.
|
|
53
|
+
const names = anchorNames(anchor);
|
|
54
|
+
/** @type {HTMLElement} */ (anchor).style.setProperty('anchor-name', [...names, anchorName].join(', '));
|
|
52
55
|
floating.style.position = 'fixed';
|
|
53
56
|
floating.style.inset = 'auto';
|
|
54
57
|
floating.style.margin = '0';
|
|
@@ -104,13 +107,29 @@ export function position(anchor, floating, options = {}) {
|
|
|
104
107
|
observer?.disconnect();
|
|
105
108
|
cleanupMode();
|
|
106
109
|
hidePopover(floating);
|
|
107
|
-
|
|
110
|
+
if (supportsAnchors) {
|
|
111
|
+
// Only this name leaves: another floating element may still be anchored to the same one.
|
|
112
|
+
const rest = anchorNames(anchor).filter((name) => name !== anchorName);
|
|
113
|
+
if (rest.length) /** @type {HTMLElement} */ (anchor).style.setProperty('anchor-name', rest.join(', '));
|
|
114
|
+
else /** @type {HTMLElement} */ (anchor).style.removeProperty('anchor-name');
|
|
115
|
+
if (anchorStyle === null && anchor.getAttribute('style') === '') anchor.removeAttribute('style');
|
|
116
|
+
}
|
|
108
117
|
restoreAttribute(floating, 'style', floatingStyle);
|
|
109
118
|
restoreAttribute(floating, 'popover', popover);
|
|
110
119
|
}
|
|
111
120
|
};
|
|
112
121
|
}
|
|
113
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The anchor names set inline on an element.
|
|
125
|
+
* @param {Element} anchor
|
|
126
|
+
* @returns {string[]}
|
|
127
|
+
*/
|
|
128
|
+
function anchorNames(anchor) {
|
|
129
|
+
return String(/** @type {HTMLElement} */ (anchor).style?.getPropertyValue('anchor-name') ?? '')
|
|
130
|
+
.split(',').map((name) => name.trim()).filter((name) => name && name !== 'none');
|
|
131
|
+
}
|
|
132
|
+
|
|
114
133
|
/** @param {string} placement @param {boolean} rtl @returns {string} */
|
|
115
134
|
function positionArea(placement, rtl) {
|
|
116
135
|
return {
|
package/src/index.js
CHANGED
|
@@ -54,7 +54,7 @@ export { position } from './core/position.js';
|
|
|
54
54
|
export { breakpoints, breakpointOf, matchBreakpoint, onBreakpoint } from './core/breakpoint.js';
|
|
55
55
|
export { MOTION_ATTRIBUTE, prefersReducedMotion, watchMotion } from './core/motion.js';
|
|
56
56
|
export { Http, zeyosService, parseResult } from './core/http.js';
|
|
57
|
-
export { setTranslator, setLanguage, getLanguage, translate, printf } from './core/i18n.js';
|
|
57
|
+
export { setTranslator, setLanguage, getLanguage, translate, printf, defaultMessages } from './core/i18n.js';
|
|
58
58
|
export { focusTrap, rovingTabindex, typeahead } from './core/keyboard.js';
|
|
59
59
|
export {
|
|
60
60
|
formatDate, parseDate, clampDate, isSameDay, addDays, addMonths, getWeekStart
|
|
@@ -123,8 +123,10 @@ export {
|
|
|
123
123
|
ActivityList, groupActivityItems, normalizeActivityItems
|
|
124
124
|
} from './components/activity-list/activity-list.js';
|
|
125
125
|
export { AccountMenu } from './components/account-menu/account-menu.js';
|
|
126
|
+
export { WorkspaceSwitcher } from './components/workspace-switcher/workspace-switcher.js';
|
|
126
127
|
export { normalizeAppItems } from './internal/app-rail.js';
|
|
127
|
-
export {
|
|
128
|
+
export { ModuleBar, partitionModules, resolveModuleColor } from './components/module-bar/module-bar.js';
|
|
129
|
+
export { AppSidebar, presentationGlyph } from './components/app-sidebar/app-sidebar.js';
|
|
128
130
|
export { Message } from './components/message/message.js';
|
|
129
131
|
export { Modal } from './components/modal/modal.js';
|
|
130
132
|
export { Dialog } from './components/dialog/dialog.js';
|
|
@@ -188,6 +190,7 @@ export {
|
|
|
188
190
|
} from './components/filter-panel/filter-panel.js';
|
|
189
191
|
export { SortControl } from './components/sort-control/sort-control.js';
|
|
190
192
|
export { ListToolbar } from './components/list-toolbar/list-toolbar.js';
|
|
193
|
+
export { ViewSettings } from './components/view-settings/view-settings.js';
|
|
191
194
|
export { Filter } from './components/filter/filter.js';
|
|
192
195
|
export {
|
|
193
196
|
filterOperators, emptyFilterAst, filterCondition, filterGroup,
|