@zeyos/zx 2.2.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +197 -0
- package/DESIGN-SYSTEM.md +136 -0
- package/MIGRATION.md +155 -4
- package/README.md +51 -14
- package/dist/types/compat/map/data.d.ts +3 -2
- package/dist/types/components/account-menu/account-menu.d.ts +144 -0
- package/dist/types/components/app-rail/app-rail.d.ts +250 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +169 -0
- package/dist/types/components/avatar/avatar.d.ts +100 -0
- package/dist/types/components/chart/chart.d.ts +321 -0
- package/dist/types/components/date-picker/month-picker.d.ts +6 -0
- package/dist/types/components/dialog/dialog.d.ts +9 -0
- package/dist/types/components/dock/dock.d.ts +495 -0
- package/dist/types/components/dropdown/dropdown.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +14 -0
- package/dist/types/components/field-upload/field-upload.d.ts +37 -4
- package/dist/types/components/form/form.d.ts +25 -3
- package/dist/types/components/grid/grid.d.ts +172 -0
- package/dist/types/components/launcher/launcher.d.ts +300 -0
- package/dist/types/components/layout/layout.d.ts +4 -4
- package/dist/types/components/menu-button/menu-button.d.ts +10 -2
- package/dist/types/components/modal/modal.d.ts +25 -0
- package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +74 -9
- package/dist/types/components/questionnaire/items.d.ts +327 -0
- package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
- package/dist/types/components/select/select.d.ts +36 -1
- package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
- package/dist/types/components/sheet/sheet.d.ts +294 -0
- package/dist/types/components/split-view/split-view.d.ts +2 -21
- package/dist/types/components/tabbox/tabbox.d.ts +12 -3
- package/dist/types/components/table/table.d.ts +219 -12
- package/dist/types/components/tooltip/tooltip.d.ts +2 -2
- package/dist/types/components/value-list/value-list.d.ts +24 -3
- package/dist/types/core/dom.d.ts +114 -0
- package/dist/types/core/drag-axis.d.ts +111 -0
- package/dist/types/core/position.d.ts +4 -2
- package/dist/types/index.d.ts +12 -2
- package/dist/zx-compat.esm.js +943 -311
- package/dist/zx-compat.esm.min.js +1 -1
- package/dist/zx-compat.global.js +7893 -2252
- package/dist/zx-compat.global.min.js +3 -3
- package/dist/zx-zeyos.esm.js +826 -114
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +1720 -243
- package/dist/zx.esm.js +8227 -2599
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +8227 -2599
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +2597 -255
- package/docs/llms.md +475 -37
- package/docs/llms.txt +21 -9
- package/package.json +4 -2
- package/src/compat/globals.js +3 -1
- package/src/compat/map/data.js +31 -9
- package/src/components/account-menu/account-menu.css +70 -0
- package/src/components/account-menu/account-menu.js +208 -0
- package/src/components/app-rail/app-rail.css +107 -0
- package/src/components/app-rail/app-rail.js +668 -0
- package/src/components/app-sidebar/app-sidebar.css +68 -0
- package/src/components/app-sidebar/app-sidebar.js +388 -0
- package/src/components/avatar/avatar.css +61 -0
- package/src/components/avatar/avatar.js +222 -0
- package/src/components/button/button.css +0 -1
- package/src/components/chart/chart.css +61 -0
- package/src/components/chart/chart.js +518 -0
- package/src/components/date-picker/date-range-picker.css +7 -0
- package/src/components/date-picker/month-picker.js +9 -0
- package/src/components/dialog/dialog.js +11 -4
- package/src/components/dock/dock.css +228 -0
- package/src/components/dock/dock.js +977 -0
- package/src/components/dropdown/dropdown.js +1 -1
- package/src/components/field/field.js +2 -0
- package/src/components/field-upload/field-upload.js +9 -4
- package/src/components/fieldset/fieldset.css +7 -0
- package/src/components/form/form.css +7 -0
- package/src/components/form/form.js +6 -3
- package/src/components/grid/grid.css +6 -0
- package/src/components/grid/grid.js +213 -0
- package/src/components/launcher/launcher.css +100 -0
- package/src/components/launcher/launcher.js +557 -0
- package/src/components/layout/layout.js +2 -2
- package/src/components/menu-button/menu-button.js +27 -2
- package/src/components/modal/modal.css +5 -1
- package/src/components/modal/modal.js +37 -2
- package/src/components/multi-value-editor/multi-value-editor.css +99 -15
- package/src/components/multi-value-editor/multi-value-editor.js +237 -19
- package/src/components/navigation-bar/navigation-bar.css +7 -0
- package/src/components/questionnaire/items.js +370 -0
- package/src/components/questionnaire/questionnaire.css +387 -0
- package/src/components/questionnaire/questionnaire.js +1033 -0
- package/src/components/search/search.css +10 -2
- package/src/components/select/select.css +6 -1
- package/src/components/select/select.js +99 -11
- package/src/components/sheet/sheet-stack.js +304 -0
- package/src/components/sheet/sheet.css +351 -0
- package/src/components/sheet/sheet.js +551 -0
- package/src/components/split-view/split-view.js +76 -161
- package/src/components/stepper/stepper.css +12 -2
- package/src/components/tabbox/tabbox.css +13 -3
- package/src/components/tabbox/tabbox.js +34 -10
- package/src/components/table/table.css +43 -0
- package/src/components/table/table.js +523 -45
- package/src/components/tooltip/tooltip.js +5 -2
- package/src/components/value-list/value-list.js +6 -3
- package/src/core/dom.js +302 -0
- package/src/core/drag-axis.js +203 -0
- package/src/core/position.js +68 -22
- package/src/index.js +19 -2
- package/styles/tokens/dark.css +15 -13
- package/styles/tokens/global.css +17 -5
- package/styles/tokens/semantic.css +14 -8
- package/styles/tokens/themes.css +72 -0
- package/styles/zx.css +11 -1
- package/dist/types/components/permission/permission.d.ts +0 -93
- package/src/components/permission/permission.css +0 -47
- package/src/components/permission/permission.js +0 -198
|
@@ -4,7 +4,10 @@ import { position } from '../../core/position.js';
|
|
|
4
4
|
import { uid } from '../../core/util.js';
|
|
5
5
|
|
|
6
6
|
/** Placements accepted by the option and understood by `position()`. */
|
|
7
|
-
const PLACEMENTS = new Set([
|
|
7
|
+
const PLACEMENTS = new Set([
|
|
8
|
+
'top', 'bottom', 'top-start', 'top-end', 'bottom-start', 'bottom-end',
|
|
9
|
+
'left', 'right', 'left-start', 'left-end', 'right-start', 'right-end'
|
|
10
|
+
]);
|
|
8
11
|
/** Trigger modes. `manual` installs no listeners at all. */
|
|
9
12
|
const TRIGGERS = new Set(['hover', 'focus', 'both', 'manual']);
|
|
10
13
|
|
|
@@ -21,7 +24,7 @@ const TRIGGERS = new Set(['hover', 'focus', 'both', 'manual']);
|
|
|
21
24
|
/**
|
|
22
25
|
* @typedef {Object} TooltipOptions
|
|
23
26
|
* @property {TooltipContent} [content=''] Bubble content, re-evaluated on each open when a function.
|
|
24
|
-
* @property {'top'|'bottom'|'top-start'|'top-end'|'bottom-start'|'bottom-end'} [placement='top'] Preferred side; flips when it would overflow.
|
|
27
|
+
* @property {'top'|'bottom'|'top-start'|'top-end'|'bottom-start'|'bottom-end'|'left'|'right'|'left-start'|'left-end'|'right-start'|'right-end'} [placement='top'] Preferred side; flips when it would overflow.
|
|
25
28
|
* @property {number} [offset=6] Gap between anchor and bubble in pixels.
|
|
26
29
|
* @property {number} [delay=400] Delay before a hover opens the tooltip, in milliseconds.
|
|
27
30
|
* @property {number} [hideDelay=80] Delay before a leave or blur closes it, in milliseconds.
|
|
@@ -10,6 +10,9 @@ import { uid } from '../../core/util.js';
|
|
|
10
10
|
* @property {boolean} [sortable=true] Whether values can be reordered.
|
|
11
11
|
* @property {boolean} [unique=true] Whether duplicate strings are rejected.
|
|
12
12
|
* @property {((value: string) => boolean|string)|null} [validate=null] Value validator.
|
|
13
|
+
* @property {(event: CustomEvent<{values: string[]}>) => void} [onchange] Values change listener.
|
|
14
|
+
* @property {(event: CustomEvent<{value: string}>) => void} [onadd] Value added listener.
|
|
15
|
+
* @property {(event: CustomEvent<{value: string}>) => void} [onremove] Value removed listener.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -320,17 +323,17 @@ function normalizeValues(values, unique) {
|
|
|
320
323
|
/**
|
|
321
324
|
* Values changed event.
|
|
322
325
|
* @event ValueList#change
|
|
323
|
-
* @type {CustomEvent<{values: string[]}>
|
|
326
|
+
* @type {CustomEvent<{values: string[]}>}
|
|
324
327
|
*/
|
|
325
328
|
|
|
326
329
|
/**
|
|
327
330
|
* Value added event.
|
|
328
331
|
* @event ValueList#add
|
|
329
|
-
* @type {CustomEvent<{value: string}>
|
|
332
|
+
* @type {CustomEvent<{value: string}>}
|
|
330
333
|
*/
|
|
331
334
|
|
|
332
335
|
/**
|
|
333
336
|
* Value removed event.
|
|
334
337
|
* @event ValueList#remove
|
|
335
|
-
* @type {CustomEvent<{value: string}>
|
|
338
|
+
* @type {CustomEvent<{value: string}>}
|
|
336
339
|
*/
|
package/src/core/dom.js
CHANGED
|
@@ -3,6 +3,7 @@ import { escapeRegExp, isElement } from './util.js';
|
|
|
3
3
|
|
|
4
4
|
/** @typedef {{toElement: () => Node|null}} ElementProvider */
|
|
5
5
|
/** @typedef {Node|string|number|ElementProvider|null|undefined|Array<DomChild>} DomChild */
|
|
6
|
+
/** @typedef {Record<string, any>} DomBuilderProperties */
|
|
6
7
|
/**
|
|
7
8
|
* @typedef {Object} HProps
|
|
8
9
|
* @property {string|string[]} [class] Class name or names.
|
|
@@ -14,6 +15,173 @@ import { escapeRegExp, isElement } from './util.js';
|
|
|
14
15
|
/** @type {Record<string, Element>|null} */
|
|
15
16
|
let activeRefs = null;
|
|
16
17
|
|
|
18
|
+
/** Properties the ZeyOS builder historically assigns through the DOM property rather than an attribute. */
|
|
19
|
+
const DOM_BUILDER_PROPERTIES = new Set([
|
|
20
|
+
'href', 'id', 'lang', 'name', 'rel', 'src', 'target', 'title', 'value'
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/** @type {WeakMap<EventTarget, Map<string, {handlers: Function[], listener: EventListener}>>} */
|
|
24
|
+
const compactEvents = new WeakMap();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} CompactTag
|
|
28
|
+
* @property {string} tag Element tag, defaulting to `div`.
|
|
29
|
+
* @property {string} id Element id.
|
|
30
|
+
* @property {string} className Space-delimited class string.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parses the compact ZeyOS builder grammar: `tag#id.class names`. The order intentionally matches
|
|
35
|
+
* the established function—class text begins at the first dot, then id is read from the remaining
|
|
36
|
+
* tag portion—so thousands of existing call sites retain the same meaning.
|
|
37
|
+
* @param {string} compact Compact element description.
|
|
38
|
+
* @returns {CompactTag}
|
|
39
|
+
*/
|
|
40
|
+
export function parseCompactTag(compact) {
|
|
41
|
+
if (typeof compact !== 'string') throw new TypeError('DOM builder tag must be a string');
|
|
42
|
+
let tag = compact;
|
|
43
|
+
let className = '';
|
|
44
|
+
let id = '';
|
|
45
|
+
let index = tag.indexOf('.');
|
|
46
|
+
if (index !== -1) {
|
|
47
|
+
className = tag.slice(index + 1);
|
|
48
|
+
tag = tag.slice(0, index);
|
|
49
|
+
}
|
|
50
|
+
index = tag.indexOf('#');
|
|
51
|
+
if (index !== -1) {
|
|
52
|
+
id = tag.slice(index + 1);
|
|
53
|
+
tag = tag.slice(0, index);
|
|
54
|
+
}
|
|
55
|
+
return { tag: tag || 'div', id, className };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* ZeyOS's compact DOM builder, now a first-class Zx primitive. Primitive content is assigned as
|
|
60
|
+
* text and object content is appended as a Node; HTML strings are never parsed.
|
|
61
|
+
*
|
|
62
|
+
* Existing syntax is preserved (`__('button.primary', {Daction: 'save'}, 'Save')`). Zx components
|
|
63
|
+
* exposing `toElement()` are the one additive extension, so a component may replace a UI factory
|
|
64
|
+
* without changing the surrounding builder call.
|
|
65
|
+
* @param {string} compact `tag#id.class names`; an omitted tag creates a `div`.
|
|
66
|
+
* @param {DomBuilderProperties|null} [properties=null] ZeyOS builder properties.
|
|
67
|
+
* @param {unknown} [content=null] Text-safe content.
|
|
68
|
+
* @returns {HTMLElement}
|
|
69
|
+
*/
|
|
70
|
+
export function compactElement(compact, properties = null, content = null) {
|
|
71
|
+
const description = parseCompactTag(compact);
|
|
72
|
+
const element = document.createElement(description.tag);
|
|
73
|
+
if (description.id) element.id = description.id;
|
|
74
|
+
if (description.className) element.className = description.className;
|
|
75
|
+
|
|
76
|
+
if (description.tag === 'a') {
|
|
77
|
+
const anchor = /** @type {HTMLAnchorElement} */ (element);
|
|
78
|
+
anchor.rel = 'noopener';
|
|
79
|
+
anchor.tabIndex = -1;
|
|
80
|
+
} else if (description.tag === 'button') {
|
|
81
|
+
/** @type {HTMLButtonElement} */ (element).type = 'button';
|
|
82
|
+
} else if (description.tag === 'input' || description.tag === 'textarea') {
|
|
83
|
+
const control = /** @type {any} */ (element);
|
|
84
|
+
control.autocapitalize = 'none';
|
|
85
|
+
control.autocomplete = 'off';
|
|
86
|
+
control.autocorrect = 'off';
|
|
87
|
+
control.spellcheck = false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (properties) applyCompactProperties(element, properties);
|
|
91
|
+
setCompactContent(element, content);
|
|
92
|
+
return element;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Exact migration-facing alias retained for existing ZeyOS builder calls. */
|
|
96
|
+
export const __ = compactElement;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Applies the established ZeyOS property rules to an existing element:
|
|
100
|
+
* `Dname` → `data-name`, `SfontSize` → `style.fontSize`, `on*` → a legacy-compatible listener,
|
|
101
|
+
* booleans/built-ins → DOM properties, and everything else → attributes.
|
|
102
|
+
* @param {HTMLElement} element Target element.
|
|
103
|
+
* @param {DomBuilderProperties} properties Properties to apply.
|
|
104
|
+
* @returns {HTMLElement} The target, for chaining.
|
|
105
|
+
*/
|
|
106
|
+
export function applyCompactProperties(element, properties) {
|
|
107
|
+
// `for…in` is intentional: the established builder also applies enumerable properties inherited
|
|
108
|
+
// from a shared descriptor prototype. Changing that during migration would silently drop them.
|
|
109
|
+
for (const name in properties ?? {}) {
|
|
110
|
+
const value = properties[name];
|
|
111
|
+
if (value == null) continue;
|
|
112
|
+
if (name[0] === 'D') {
|
|
113
|
+
element.setAttribute(`data-${name.slice(1)}`, String(value));
|
|
114
|
+
} else if (name[0] === 'S') {
|
|
115
|
+
element.style[name.slice(1)] = value;
|
|
116
|
+
} else if (name.startsWith('on')) {
|
|
117
|
+
if (typeof value !== 'function') {
|
|
118
|
+
throw new TypeError(`Compact DOM event ${name} must be a function`);
|
|
119
|
+
}
|
|
120
|
+
onCompactEvent(element, name.slice(2), value);
|
|
121
|
+
} else if (typeof value === 'boolean' || DOM_BUILDER_PROPERTIES.has(name)) {
|
|
122
|
+
element[name] = value;
|
|
123
|
+
} else {
|
|
124
|
+
element.setAttribute(name, String(value));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return element;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Appends one child and returns the parent: the pure-function equivalent of `parent._(child)`.
|
|
132
|
+
* Primitive values become text; a component exposing `toElement()` is accepted additively.
|
|
133
|
+
* @param {Node} parent Parent node.
|
|
134
|
+
* @param {unknown} child Child content.
|
|
135
|
+
* @returns {Node}
|
|
136
|
+
*/
|
|
137
|
+
export function appendContent(parent, child) {
|
|
138
|
+
if (child == null) return parent;
|
|
139
|
+
const resolved = resolveCompactProvider(child);
|
|
140
|
+
if (resolved == null) return parent;
|
|
141
|
+
if (typeof resolved === 'object') parent.appendChild(/** @type {Node} */ (resolved));
|
|
142
|
+
else parent.appendChild(document.createTextNode(String(resolved)));
|
|
143
|
+
return parent;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Creates and appends a compact element, returning the parent: the pure equivalent of `parent.__`.
|
|
148
|
+
* @param {Node} parent Parent node.
|
|
149
|
+
* @param {string} compact Compact element description.
|
|
150
|
+
* @param {DomBuilderProperties|null} [properties=null] Builder properties.
|
|
151
|
+
* @param {unknown} [content=null] Text-safe content.
|
|
152
|
+
* @returns {Node}
|
|
153
|
+
*/
|
|
154
|
+
export function appendCompact(parent, compact, properties = null, content = null) {
|
|
155
|
+
parent.appendChild(compactElement(compact, properties, content));
|
|
156
|
+
return parent;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Creates and appends a compact element, returning the child: the pure equivalent of `parent.$__`.
|
|
161
|
+
* @param {Node} parent Parent node.
|
|
162
|
+
* @param {string} compact Compact element description.
|
|
163
|
+
* @param {DomBuilderProperties|null} [properties=null] Builder properties.
|
|
164
|
+
* @param {unknown} [content=null] Text-safe content.
|
|
165
|
+
* @returns {HTMLElement}
|
|
166
|
+
*/
|
|
167
|
+
export function appendCompactChild(parent, compact, properties = null, content = null) {
|
|
168
|
+
const child = compactElement(compact, properties, content);
|
|
169
|
+
parent.appendChild(child);
|
|
170
|
+
return child;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Creates a document fragment with normalized modern Zx children. Unlike the exact compact
|
|
175
|
+
* builder, this convenience accepts arrays and flattens them like `h()`.
|
|
176
|
+
* @param {...(DomChild|boolean)} children Fragment children.
|
|
177
|
+
* @returns {DocumentFragment}
|
|
178
|
+
*/
|
|
179
|
+
export function fragment(...children) {
|
|
180
|
+
const result = document.createDocumentFragment();
|
|
181
|
+
appendChildren(result, /** @type {DomChild[]} */ (children));
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
17
185
|
/**
|
|
18
186
|
* Creates an element and appends normalized children.
|
|
19
187
|
* @param {string} tag Element tag name.
|
|
@@ -258,3 +426,137 @@ function appendChildren(parent, children) {
|
|
|
258
426
|
}
|
|
259
427
|
}
|
|
260
428
|
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Applies the compact builder's exact single-content contract. Objects are passed to native
|
|
432
|
+
* `appendChild`, while primitives replace textContent. Providers are the explicit Zx extension.
|
|
433
|
+
* @param {HTMLElement} element Target element.
|
|
434
|
+
* @param {unknown} content Builder content.
|
|
435
|
+
* @returns {void}
|
|
436
|
+
*/
|
|
437
|
+
function setCompactContent(element, content) {
|
|
438
|
+
if (content == null) return;
|
|
439
|
+
const resolved = resolveCompactProvider(content);
|
|
440
|
+
if (resolved == null) return;
|
|
441
|
+
if (typeof resolved === 'object') element.appendChild(/** @type {Node} */ (resolved));
|
|
442
|
+
else element.textContent = String(resolved);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Resolves the one additive component-provider extension without normalizing any other object.
|
|
447
|
+
* Arrays and plain objects therefore retain the native `appendChild` failure contract.
|
|
448
|
+
* @param {unknown} value Candidate content.
|
|
449
|
+
* @returns {unknown}
|
|
450
|
+
*/
|
|
451
|
+
function resolveCompactProvider(value) {
|
|
452
|
+
if (value && typeof value === 'object') {
|
|
453
|
+
// A native Node always wins, even if application code attached a `toElement()` method to it.
|
|
454
|
+
// Classic passes every object straight to appendChild; provider resolution is additive only
|
|
455
|
+
// for component-like objects that are not already DOM nodes.
|
|
456
|
+
if (typeof (/** @type {any} */ (value)).nodeType === 'number') return value;
|
|
457
|
+
if (typeof (/** @type {any} */ (value)).toElement === 'function') {
|
|
458
|
+
return (/** @type {ElementProvider} */ (value)).toElement();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Registers an event with the established ZeyOS callback contract: handlers run newest-first,
|
|
466
|
+
* receive `(event, event.target)` (or the key code for keyboard events), and may return false to
|
|
467
|
+
* prevent the default action. This exported registry is shared by compact properties, removal,
|
|
468
|
+
* and synthetic firing so the legacy `DOM` facade can delegate all three operations to it.
|
|
469
|
+
* @param {EventTarget} target Event target.
|
|
470
|
+
* @param {string} type Event name.
|
|
471
|
+
* @param {Function} handler Listener.
|
|
472
|
+
* @param {boolean} [passive=false] Native passive-listener hint.
|
|
473
|
+
* @returns {EventTarget}
|
|
474
|
+
*/
|
|
475
|
+
export function onCompactEvent(target, type, handler, passive = false) {
|
|
476
|
+
if (typeof handler !== 'function') throw new TypeError('Compact DOM event handler must be a function');
|
|
477
|
+
const eventType = String(type);
|
|
478
|
+
let events = compactEvents.get(target);
|
|
479
|
+
if (!events) {
|
|
480
|
+
events = new Map();
|
|
481
|
+
compactEvents.set(target, events);
|
|
482
|
+
}
|
|
483
|
+
const registered = events.get(eventType);
|
|
484
|
+
if (registered) {
|
|
485
|
+
registered.handlers.push(handler);
|
|
486
|
+
return target;
|
|
487
|
+
}
|
|
488
|
+
const handlers = [handler];
|
|
489
|
+
/** @type {EventListener} */
|
|
490
|
+
const listener = (event) => dispatchCompactEvent(target, handlers, event);
|
|
491
|
+
events.set(eventType, { handlers, listener });
|
|
492
|
+
try {
|
|
493
|
+
target.addEventListener(eventType, listener, { passive });
|
|
494
|
+
} catch {
|
|
495
|
+
target.addEventListener(eventType, listener);
|
|
496
|
+
}
|
|
497
|
+
return target;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Removes one registered handler, matching the established duplicate-registration behavior.
|
|
502
|
+
* @param {EventTarget} target Event target.
|
|
503
|
+
* @param {string} type Event name.
|
|
504
|
+
* @param {Function} handler Handler to remove.
|
|
505
|
+
* @returns {EventTarget}
|
|
506
|
+
*/
|
|
507
|
+
export function offCompactEvent(target, type, handler) {
|
|
508
|
+
const eventType = String(type);
|
|
509
|
+
const events = compactEvents.get(target);
|
|
510
|
+
const registered = events?.get(eventType);
|
|
511
|
+
if (!registered) return target;
|
|
512
|
+
const index = registered.handlers.indexOf(handler);
|
|
513
|
+
if (index === -1) return target;
|
|
514
|
+
registered.handlers.splice(index, 1);
|
|
515
|
+
if (registered.handlers.length === 0) {
|
|
516
|
+
target.removeEventListener(eventType, registered.listener);
|
|
517
|
+
events.delete(eventType);
|
|
518
|
+
if (events.size === 0) compactEvents.delete(target);
|
|
519
|
+
}
|
|
520
|
+
return target;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Invokes compact handlers without dispatching unrelated native listeners.
|
|
525
|
+
* @param {EventTarget} target Event target.
|
|
526
|
+
* @param {string} type Event name.
|
|
527
|
+
* @param {Event|Record<string, any>|null} [event=null] Event-like object.
|
|
528
|
+
* @returns {EventTarget}
|
|
529
|
+
*/
|
|
530
|
+
export function fireCompactEvent(target, type, event = null) {
|
|
531
|
+
const eventType = String(type);
|
|
532
|
+
const events = compactEvents.get(target);
|
|
533
|
+
// Match DOM.fireEvt's guard: a requested type must be registered before an event is considered.
|
|
534
|
+
if (!events?.has(eventType)) return target;
|
|
535
|
+
const actual = event ?? compactSyntheticEvent(target, eventType);
|
|
536
|
+
// DOM.handleEvt then dispatches by the supplied event's own type. This distinction matters for
|
|
537
|
+
// hand-built event objects even though normal ZeyOS callers omit the optional event argument.
|
|
538
|
+
const registered = events.get(String((/** @type {any} */ (actual)).type));
|
|
539
|
+
if (registered) dispatchCompactEvent(target, registered.handlers, actual);
|
|
540
|
+
return target;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** @param {EventTarget} target @param {Function[]} handlers @param {Event|Record<string, any>} event */
|
|
544
|
+
function dispatchCompactEvent(target, handlers, event) {
|
|
545
|
+
const value = /** @type {any} */ (event);
|
|
546
|
+
const secondary = String(value.type).startsWith('key') ? value.which : value.target;
|
|
547
|
+
for (let index = handlers.length - 1; index >= 0; index -= 1) {
|
|
548
|
+
if (handlers[index].call(target, event, secondary) === false) value.preventDefault();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** @param {EventTarget} target @param {string} type @returns {Record<string, any>} */
|
|
553
|
+
function compactSyntheticEvent(target, type) {
|
|
554
|
+
const event = {
|
|
555
|
+
target,
|
|
556
|
+
type,
|
|
557
|
+
which: -1,
|
|
558
|
+
defaultPrevented: false,
|
|
559
|
+
preventDefault() { event.defaultPrevented = true; }
|
|
560
|
+
};
|
|
561
|
+
return event;
|
|
562
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** Pixels an arrow key moves a handle. */
|
|
4
|
+
export const STEP = 16;
|
|
5
|
+
/** Pixels a shifted arrow key moves a handle. */
|
|
6
|
+
export const STEP_LARGE = 64;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} DragAxisOptions
|
|
10
|
+
* @property {'horizontal'|'vertical'} orientation Axis the handle moves along. `horizontal` reads
|
|
11
|
+
* `clientX`, `vertical` reads `clientY`.
|
|
12
|
+
* @property {(event: PointerEvent) => boolean|void} [onStart] Called on pointer-down before the
|
|
13
|
+
* drag begins. Return `false` to refuse it — the usual reason being that there is nothing to
|
|
14
|
+
* resize on either side of the handle.
|
|
15
|
+
* @property {(delta: number) => void} onMove Called at most once per frame with the signed pixel
|
|
16
|
+
* distance travelled along the axis since pointer-down.
|
|
17
|
+
* @property {(delta: number, moved: boolean) => void} [onEnd] Called once the pointer is released.
|
|
18
|
+
* `moved` is false for a press that never travelled, which is a click rather than a drag.
|
|
19
|
+
* @property {(direction: -1|1, large: boolean) => void} [onStep] Called for an arrow key along the
|
|
20
|
+
* axis. `large` is true while Shift is held.
|
|
21
|
+
* @property {(edge: 'min'|'max') => void} [onJump] Called for Home and End.
|
|
22
|
+
* @property {() => void} [onActivate] Called for Enter and Space, which components use for the
|
|
23
|
+
* collapse affordance a splitter is expected to offer.
|
|
24
|
+
* @property {() => boolean} [disabled] Consulted before every interaction.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} DragAxisController
|
|
29
|
+
* @property {() => boolean} isDragging Whether a drag is in flight.
|
|
30
|
+
* @property {() => void} cancel Ends any drag in flight without reporting it.
|
|
31
|
+
* @property {() => void} destroy Removes listeners and cancels any drag in flight.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pointer and keyboard mechanics for anything dragged along one axis — a split divider, a dock
|
|
36
|
+
* divider, the resizable edge of a sheet.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately knows nothing about sizes. It reports the signed distance travelled and leaves
|
|
39
|
+
* every consumer to do its own arithmetic, because the three that need it disagree about what a
|
|
40
|
+
* drag means: a split view moves one boundary, a dock moves size between two neighbours, and a
|
|
41
|
+
* sheet grows in the opposite direction depending on which edge it is attached to. What they do
|
|
42
|
+
* share is the fiddly part — pointer capture that survives the pointer leaving the handle, one
|
|
43
|
+
* write per frame however fast the pointer reports, telling a click apart from a zero-distance
|
|
44
|
+
* drag, and the WAI-ARIA keyboard map.
|
|
45
|
+
*
|
|
46
|
+
* @param {Element} handle Element the pointer grabs; also the keyboard target.
|
|
47
|
+
* @param {DragAxisOptions} options Axis and callbacks.
|
|
48
|
+
* @returns {DragAxisController}
|
|
49
|
+
*/
|
|
50
|
+
export function dragAxis(handle, options) {
|
|
51
|
+
if (!handle) throw new TypeError('dragAxis requires a handle element');
|
|
52
|
+
if (typeof options?.onMove !== 'function') throw new TypeError('dragAxis requires an onMove callback');
|
|
53
|
+
|
|
54
|
+
const vertical = options.orientation === 'vertical';
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const { signal } = controller;
|
|
57
|
+
|
|
58
|
+
let dragging = false;
|
|
59
|
+
/** @type {number|null} */
|
|
60
|
+
let pointerId = null;
|
|
61
|
+
let startClient = 0;
|
|
62
|
+
let client = 0;
|
|
63
|
+
let moved = false;
|
|
64
|
+
/** Handle of the frame a pending update is scheduled in; `0` if none. */
|
|
65
|
+
let frame = 0;
|
|
66
|
+
|
|
67
|
+
/** @param {PointerEvent} event @returns {number} */
|
|
68
|
+
const axis = (event) => (vertical ? event.clientY : event.clientX);
|
|
69
|
+
|
|
70
|
+
/** @returns {boolean} */
|
|
71
|
+
const blocked = () => Boolean(options.disabled?.());
|
|
72
|
+
|
|
73
|
+
/** @returns {void} */
|
|
74
|
+
const releasePointer = () => {
|
|
75
|
+
const id = pointerId;
|
|
76
|
+
pointerId = null;
|
|
77
|
+
if (id === null) return;
|
|
78
|
+
try {
|
|
79
|
+
/** @type {HTMLElement} */ (handle).releasePointerCapture(id);
|
|
80
|
+
} catch {
|
|
81
|
+
// The capture is already gone once the pointer was cancelled or the node was detached.
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** @returns {void} */
|
|
86
|
+
const cancelFrame = () => {
|
|
87
|
+
if (frame === 0) return;
|
|
88
|
+
cancelAnimationFrame(frame);
|
|
89
|
+
frame = 0;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** @returns {void} */
|
|
93
|
+
const finish = () => {
|
|
94
|
+
dragging = false;
|
|
95
|
+
moved = false;
|
|
96
|
+
cancelFrame();
|
|
97
|
+
releasePointer();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
handle.addEventListener('pointerdown', (event) => {
|
|
101
|
+
const pointer = /** @type {PointerEvent} */ (event);
|
|
102
|
+
if (blocked() || dragging || pointer.button !== 0) return;
|
|
103
|
+
if (options.onStart?.(pointer) === false) return;
|
|
104
|
+
/*
|
|
105
|
+
* No preventDefault(): cancelling pointerdown suppresses the compatibility mouse events a
|
|
106
|
+
* double-click reset is built on. Handles set `user-select: none` in CSS to stop the text
|
|
107
|
+
* selection this would otherwise be guarding against.
|
|
108
|
+
*/
|
|
109
|
+
dragging = true;
|
|
110
|
+
pointerId = pointer.pointerId;
|
|
111
|
+
startClient = axis(pointer);
|
|
112
|
+
client = startClient;
|
|
113
|
+
moved = false;
|
|
114
|
+
try {
|
|
115
|
+
/** @type {HTMLElement} */ (handle).setPointerCapture(pointer.pointerId);
|
|
116
|
+
} catch {
|
|
117
|
+
// A pointer that ended between the event and this call cannot be captured; the drag simply
|
|
118
|
+
// finishes on the next pointerup.
|
|
119
|
+
}
|
|
120
|
+
}, { signal });
|
|
121
|
+
|
|
122
|
+
handle.addEventListener('pointermove', (event) => {
|
|
123
|
+
const pointer = /** @type {PointerEvent} */ (event);
|
|
124
|
+
if (!dragging || pointer.pointerId !== pointerId) return;
|
|
125
|
+
client = axis(pointer);
|
|
126
|
+
if (client !== startClient) moved = true;
|
|
127
|
+
if (frame !== 0) return;
|
|
128
|
+
frame = requestAnimationFrame(() => {
|
|
129
|
+
frame = 0;
|
|
130
|
+
if (dragging) options.onMove(client - startClient);
|
|
131
|
+
});
|
|
132
|
+
}, { signal });
|
|
133
|
+
|
|
134
|
+
/** @param {Event} event @returns {void} */
|
|
135
|
+
const end = (event) => {
|
|
136
|
+
const pointer = /** @type {PointerEvent} */ (event);
|
|
137
|
+
if (!dragging || pointer.pointerId !== pointerId) return;
|
|
138
|
+
cancelFrame();
|
|
139
|
+
client = axis(pointer);
|
|
140
|
+
const travelled = moved;
|
|
141
|
+
const delta = client - startClient;
|
|
142
|
+
finish();
|
|
143
|
+
options.onEnd?.(delta, travelled);
|
|
144
|
+
};
|
|
145
|
+
handle.addEventListener('pointerup', end, { signal });
|
|
146
|
+
handle.addEventListener('pointercancel', end, { signal });
|
|
147
|
+
|
|
148
|
+
handle.addEventListener('keydown', (event) => {
|
|
149
|
+
const key = /** @type {KeyboardEvent} */ (event);
|
|
150
|
+
if (blocked()) return;
|
|
151
|
+
if ((key.key === 'Enter' || key.key === ' ') && options.onActivate) {
|
|
152
|
+
key.preventDefault();
|
|
153
|
+
options.onActivate();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (key.key === 'Home' || key.key === 'End') {
|
|
157
|
+
if (!options.onJump) return;
|
|
158
|
+
key.preventDefault();
|
|
159
|
+
options.onJump(key.key === 'Home' ? 'min' : 'max');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const decrease = vertical ? 'ArrowUp' : 'ArrowLeft';
|
|
163
|
+
const increase = vertical ? 'ArrowDown' : 'ArrowRight';
|
|
164
|
+
if (key.key !== decrease && key.key !== increase) return;
|
|
165
|
+
if (!options.onStep) return;
|
|
166
|
+
key.preventDefault();
|
|
167
|
+
options.onStep(key.key === increase ? 1 : -1, key.shiftKey);
|
|
168
|
+
}, { signal });
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
isDragging: () => dragging,
|
|
172
|
+
cancel() {
|
|
173
|
+
if (!dragging) return;
|
|
174
|
+
finish();
|
|
175
|
+
},
|
|
176
|
+
destroy() {
|
|
177
|
+
finish();
|
|
178
|
+
controller.abort();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolves a size option to pixels without touching the DOM.
|
|
185
|
+
*
|
|
186
|
+
* Numbers, bare numeric strings, and `px` lengths are pixels; a percentage resolves against
|
|
187
|
+
* `total`. Every other CSS length — `rem`, `vh`, `calc()` — needs the browser, so it returns `NaN`
|
|
188
|
+
* and the caller hands the value to CSS instead of guessing at it.
|
|
189
|
+
* @param {number|string} value Size option.
|
|
190
|
+
* @param {number} total Container size along the split axis, in pixels.
|
|
191
|
+
* @returns {number} Pixels, or `NaN` when only the browser can resolve the value.
|
|
192
|
+
*/
|
|
193
|
+
export function resolveSize(value, total) {
|
|
194
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : Number.NaN;
|
|
195
|
+
if (typeof value !== 'string') return Number.NaN;
|
|
196
|
+
const match = /^([+-]?(?:\d+\.?\d*|\.\d+))(px|%)?$/i.exec(value.trim());
|
|
197
|
+
if (!match) return Number.NaN;
|
|
198
|
+
const amount = Number(match[1]);
|
|
199
|
+
if (!Number.isFinite(amount)) return Number.NaN;
|
|
200
|
+
if ((match[2] ?? '').toLowerCase() !== '%') return amount;
|
|
201
|
+
const span = Number(total);
|
|
202
|
+
return Number.isFinite(span) ? (amount / 100) * span : Number.NaN;
|
|
203
|
+
}
|