@zeyos/zx 2.1.0 → 2.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 +39 -0
- package/README.md +15 -0
- package/dist/types/compat/base.d.ts +156 -0
- package/dist/types/compat/globals.d.ts +20 -0
- package/dist/types/compat/index.d.ts +128 -0
- package/dist/types/compat/map/basic.d.ts +175 -0
- package/dist/types/compat/map/data.d.ts +102 -0
- package/dist/types/compat/map/date-time.d.ts +80 -0
- package/dist/types/compat/map/factory.d.ts +9 -0
- package/dist/types/compat/map/form.d.ts +98 -0
- package/dist/types/compat/map/helpers.d.ts +12 -0
- package/dist/types/compat/map/http.d.ts +36 -0
- package/dist/types/compat/map/layout.d.ts +106 -0
- package/dist/types/compat/map/options.d.ts +80 -0
- package/dist/types/compat/map/select.d.ts +95 -0
- package/dist/types/compat/map/stubs.d.ts +32 -0
- package/dist/types/compat/map/utilities.d.ts +37 -0
- package/dist/types/compat-entry.d.ts +5 -0
- package/dist/types/components/badge/badge.d.ts +46 -0
- package/dist/types/components/breadcrumb/breadcrumb.d.ts +200 -0
- package/dist/types/components/button/button.d.ts +42 -0
- package/dist/types/components/check-button/check-button.d.ts +98 -0
- package/dist/types/components/checklist/checklist.d.ts +143 -0
- package/dist/types/components/checklist/field-adapter.d.ts +5 -0
- package/dist/types/components/context-menu/context-menu.d.ts +130 -0
- package/dist/types/components/copy/copy-button.d.ts +48 -0
- package/dist/types/components/copy/copy-input.d.ts +91 -0
- package/dist/types/components/data-filter/data-filter.d.ts +250 -0
- package/dist/types/components/data-filter/filter-core.d.ts +52 -0
- package/dist/types/components/date-picker/date-picker.d.ts +137 -0
- package/dist/types/components/date-picker/date-range-picker.d.ts +350 -0
- package/dist/types/components/date-picker/field-adapter.d.ts +6 -0
- package/dist/types/components/date-picker/month-picker.d.ts +75 -0
- package/dist/types/components/date-picker/range-field-adapter.d.ts +7 -0
- package/dist/types/components/date-picker/time-picker.d.ts +127 -0
- package/dist/types/components/datebox/date-range-box.d.ts +218 -0
- package/dist/types/components/datebox/datebox.d.ts +160 -0
- package/dist/types/components/datebox/field-adapter.d.ts +5 -0
- package/dist/types/components/dialog/dialog.d.ts +284 -0
- package/dist/types/components/dropdown/dropdown.d.ts +118 -0
- package/dist/types/components/empty-state/empty-state.d.ts +38 -0
- package/dist/types/components/field/field.d.ts +231 -0
- package/dist/types/components/field-upload/field-adapter.d.ts +5 -0
- package/dist/types/components/field-upload/field-upload.d.ts +213 -0
- package/dist/types/components/fieldset/fieldset.d.ts +125 -0
- package/dist/types/components/finder/finder.d.ts +252 -0
- package/dist/types/components/form/form.d.ts +194 -0
- package/dist/types/components/groupbox/groupbox.d.ts +89 -0
- package/dist/types/components/layout/layout.d.ts +132 -0
- package/dist/types/components/loading/inline-loading.d.ts +86 -0
- package/dist/types/components/loading/progress-bar.d.ts +168 -0
- package/dist/types/components/loading/spinner.d.ts +34 -0
- package/dist/types/components/master-panel/master-panel.d.ts +112 -0
- package/dist/types/components/menu-button/menu-button.d.ts +142 -0
- package/dist/types/components/message/message.d.ts +212 -0
- package/dist/types/components/modal/modal.d.ts +105 -0
- package/dist/types/components/multi-value-editor/field-adapter.d.ts +5 -0
- package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +66 -0
- package/dist/types/components/navigation-bar/navigation-bar.d.ts +229 -0
- package/dist/types/components/number-field/field-adapter.d.ts +5 -0
- package/dist/types/components/number-field/number-field.d.ts +292 -0
- package/dist/types/components/pagination/pagination.d.ts +267 -0
- package/dist/types/components/panel/panel.d.ts +194 -0
- package/dist/types/components/permission/permission.d.ts +93 -0
- package/dist/types/components/rating/field-adapter.d.ts +5 -0
- package/dist/types/components/rating/rating.d.ts +213 -0
- package/dist/types/components/search/search.d.ts +98 -0
- package/dist/types/components/select/field-adapter.d.ts +5 -0
- package/dist/types/components/select/filter.d.ts +11 -0
- package/dist/types/components/select/select.d.ts +235 -0
- package/dist/types/components/skeleton/skeleton.d.ts +97 -0
- package/dist/types/components/slider/field-adapter.d.ts +5 -0
- package/dist/types/components/slider/slider.d.ts +216 -0
- package/dist/types/components/split-view/split-view.d.ts +335 -0
- package/dist/types/components/stepper/stepper.d.ts +314 -0
- package/dist/types/components/stepper/steps.d.ts +131 -0
- package/dist/types/components/tabbox/tabbox.d.ts +230 -0
- package/dist/types/components/table/sort.d.ts +49 -0
- package/dist/types/components/table/table.d.ts +872 -0
- package/dist/types/components/tag-picker/field-adapter.d.ts +5 -0
- package/dist/types/components/tag-picker/tag-picker.d.ts +375 -0
- package/dist/types/components/timebox/field-adapter.d.ts +5 -0
- package/dist/types/components/timebox/timebox.d.ts +121 -0
- package/dist/types/components/toggle/field-adapter.d.ts +5 -0
- package/dist/types/components/toggle/toggle.d.ts +94 -0
- package/dist/types/components/toolbar/toolbar.d.ts +230 -0
- package/dist/types/components/tooltip/tooltip.d.ts +183 -0
- package/dist/types/components/tree/hierarchy.d.ts +113 -0
- package/dist/types/components/tree/tree.d.ts +383 -0
- package/dist/types/components/truncate/truncate.d.ts +61 -0
- package/dist/types/components/value-list/field-adapter.d.ts +5 -0
- package/dist/types/components/value-list/value-list.d.ts +143 -0
- package/dist/types/core/breakpoint.d.ts +68 -0
- package/dist/types/core/component.d.ts +115 -0
- package/dist/types/core/date.d.ts +50 -0
- package/dist/types/core/dom.d.ts +86 -0
- package/dist/types/core/export.d.ts +70 -0
- package/dist/types/core/fontawesome.d.ts +107 -0
- package/dist/types/core/format.d.ts +107 -0
- package/dist/types/core/http.d.ts +122 -0
- package/dist/types/core/i18n.d.ts +35 -0
- package/dist/types/core/icons.d.ts +137 -0
- package/dist/types/core/keyboard.d.ts +72 -0
- package/dist/types/core/position.d.ts +45 -0
- package/dist/types/core/storage.d.ts +46 -0
- package/dist/types/core/util.d.ts +117 -0
- package/dist/types/elements/define.d.ts +73 -0
- package/dist/types/elements/reflect.d.ts +27 -0
- package/dist/types/index.d.ts +79 -0
- package/dist/types/zeyos/connect.d.ts +65 -0
- package/dist/types/zeyos/form.d.ts +140 -0
- package/dist/types/zeyos/icons.d.ts +104 -0
- package/dist/types/zeyos/index.d.ts +12 -0
- package/dist/types/zeyos/modules.d.ts +110 -0
- package/dist/types/zeyos/query.d.ts +157 -0
- package/dist/types/zeyos/schema.d.ts +79 -0
- package/dist/types/zeyos/select.d.ts +167 -0
- package/dist/types/zeyos/table.d.ts +205 -0
- package/dist/zx-compat.esm.js +362 -33
- package/dist/zx-compat.esm.min.js +1 -1
- package/dist/zx-compat.global.js +359 -67
- package/dist/zx-compat.global.min.js +3 -3
- package/dist/zx-zeyos.esm.js +342 -43
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +68 -6
- package/dist/zx.esm.js +346 -67
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +346 -67
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +48 -2
- package/docs/llms.md +28 -0
- package/package.json +37 -9
- package/src/compat/base.js +291 -0
- package/src/compat/globals.js +165 -0
- package/src/compat/index.js +84 -0
- package/src/compat/map/basic.js +464 -0
- package/src/compat/map/data.js +232 -0
- package/src/compat/map/date-time.js +177 -0
- package/src/compat/map/factory.js +45 -0
- package/src/compat/map/form.js +330 -0
- package/src/compat/map/helpers.js +46 -0
- package/src/compat/map/http.js +117 -0
- package/src/compat/map/layout.js +256 -0
- package/src/compat/map/options.js +267 -0
- package/src/compat/map/select.js +198 -0
- package/src/compat/map/stubs.js +26 -0
- package/src/compat/map/utilities.js +113 -0
- package/src/compat-entry.js +11 -0
- package/src/components/badge/badge.css +147 -0
- package/src/components/badge/badge.js +92 -0
- package/src/components/breadcrumb/breadcrumb.css +105 -0
- package/src/components/breadcrumb/breadcrumb.js +331 -0
- package/src/components/button/button.css +104 -0
- package/src/components/button/button.js +76 -0
- package/src/components/check-button/check-button.css +66 -0
- package/src/components/check-button/check-button.js +167 -0
- package/src/components/checklist/checklist.css +65 -0
- package/src/components/checklist/checklist.js +282 -0
- package/src/components/checklist/field-adapter.js +46 -0
- package/src/components/context-menu/context-menu.css +54 -0
- package/src/components/context-menu/context-menu.js +334 -0
- package/src/components/copy/copy-button.css +27 -0
- package/src/components/copy/copy-button.js +78 -0
- package/src/components/copy/copy-input.css +59 -0
- package/src/components/copy/copy-input.js +123 -0
- package/src/components/data-filter/data-filter.css +53 -0
- package/src/components/data-filter/data-filter.js +400 -0
- package/src/components/data-filter/filter-core.js +128 -0
- package/src/components/date-picker/date-picker.css +181 -0
- package/src/components/date-picker/date-picker.js +500 -0
- package/src/components/date-picker/date-range-picker.css +264 -0
- package/src/components/date-picker/date-range-picker.js +990 -0
- package/src/components/date-picker/field-adapter.js +36 -0
- package/src/components/date-picker/month-picker.js +223 -0
- package/src/components/date-picker/range-field-adapter.js +31 -0
- package/src/components/date-picker/time-picker.css +191 -0
- package/src/components/date-picker/time-picker.js +406 -0
- package/src/components/datebox/date-range-box.css +128 -0
- package/src/components/datebox/date-range-box.js +492 -0
- package/src/components/datebox/datebox.css +108 -0
- package/src/components/datebox/datebox.js +358 -0
- package/src/components/datebox/field-adapter.js +34 -0
- package/src/components/dialog/dialog.css +74 -0
- package/src/components/dialog/dialog.js +450 -0
- package/src/components/dropdown/dropdown.css +24 -0
- package/src/components/dropdown/dropdown.js +288 -0
- package/src/components/empty-state/empty-state.css +80 -0
- package/src/components/empty-state/empty-state.js +92 -0
- package/src/components/field/field.css +127 -0
- package/src/components/field/field.js +605 -0
- package/src/components/field-upload/field-adapter.js +21 -0
- package/src/components/field-upload/field-upload.css +108 -0
- package/src/components/field-upload/field-upload.js +475 -0
- package/src/components/fieldset/fieldset.css +37 -0
- package/src/components/fieldset/fieldset.js +307 -0
- package/src/components/finder/finder.css +98 -0
- package/src/components/finder/finder.js +509 -0
- package/src/components/form/form.css +32 -0
- package/src/components/form/form.js +302 -0
- package/src/components/groupbox/groupbox.css +51 -0
- package/src/components/groupbox/groupbox.js +199 -0
- package/src/components/layout/layout.css +117 -0
- package/src/components/layout/layout.js +136 -0
- package/src/components/loading/inline-loading.css +34 -0
- package/src/components/loading/inline-loading.js +140 -0
- package/src/components/loading/progress-bar.css +115 -0
- package/src/components/loading/progress-bar.js +259 -0
- package/src/components/loading/spinner.css +58 -0
- package/src/components/loading/spinner.js +57 -0
- package/src/components/master-panel/master-panel.css +51 -0
- package/src/components/master-panel/master-panel.js +203 -0
- package/src/components/menu-button/menu-button.css +49 -0
- package/src/components/menu-button/menu-button.js +294 -0
- package/src/components/message/message.css +122 -0
- package/src/components/message/message.js +499 -0
- package/src/components/modal/modal.css +47 -0
- package/src/components/modal/modal.js +203 -0
- package/src/components/multi-value-editor/field-adapter.js +44 -0
- package/src/components/multi-value-editor/multi-value-editor.css +56 -0
- package/src/components/multi-value-editor/multi-value-editor.js +170 -0
- package/src/components/navigation-bar/navigation-bar.css +130 -0
- package/src/components/navigation-bar/navigation-bar.js +450 -0
- package/src/components/number-field/field-adapter.js +40 -0
- package/src/components/number-field/number-field.css +77 -0
- package/src/components/number-field/number-field.js +545 -0
- package/src/components/pagination/pagination.css +95 -0
- package/src/components/pagination/pagination.js +481 -0
- package/src/components/panel/panel.css +123 -0
- package/src/components/panel/panel.js +336 -0
- package/src/components/permission/permission.css +47 -0
- package/src/components/permission/permission.js +198 -0
- package/src/components/rating/field-adapter.js +34 -0
- package/src/components/rating/rating.css +97 -0
- package/src/components/rating/rating.js +395 -0
- package/src/components/search/search.css +70 -0
- package/src/components/search/search.js +193 -0
- package/src/components/select/field-adapter.js +24 -0
- package/src/components/select/filter.js +39 -0
- package/src/components/select/select.css +180 -0
- package/src/components/select/select.js +663 -0
- package/src/components/skeleton/skeleton.css +76 -0
- package/src/components/skeleton/skeleton.js +139 -0
- package/src/components/slider/field-adapter.js +36 -0
- package/src/components/slider/slider.css +181 -0
- package/src/components/slider/slider.js +399 -0
- package/src/components/split-view/split-view.css +139 -0
- package/src/components/split-view/split-view.js +806 -0
- package/src/components/stepper/stepper.css +234 -0
- package/src/components/stepper/stepper.js +459 -0
- package/src/components/stepper/steps.js +131 -0
- package/src/components/tabbox/tabbox.css +281 -0
- package/src/components/tabbox/tabbox.js +484 -0
- package/src/components/table/sort.js +85 -0
- package/src/components/table/table.css +364 -0
- package/src/components/table/table.js +1693 -0
- package/src/components/tag-picker/field-adapter.js +35 -0
- package/src/components/tag-picker/tag-picker.css +169 -0
- package/src/components/tag-picker/tag-picker.js +837 -0
- package/src/components/timebox/field-adapter.js +24 -0
- package/src/components/timebox/timebox.css +51 -0
- package/src/components/timebox/timebox.js +276 -0
- package/src/components/toggle/field-adapter.js +24 -0
- package/src/components/toggle/toggle.css +66 -0
- package/src/components/toggle/toggle.js +150 -0
- package/src/components/toolbar/toolbar.css +81 -0
- package/src/components/toolbar/toolbar.js +511 -0
- package/src/components/tooltip/tooltip.css +36 -0
- package/src/components/tooltip/tooltip.js +420 -0
- package/src/components/tree/hierarchy.js +151 -0
- package/src/components/tree/tree.css +99 -0
- package/src/components/tree/tree.js +784 -0
- package/src/components/truncate/truncate.css +20 -0
- package/src/components/truncate/truncate.js +92 -0
- package/src/components/value-list/field-adapter.js +24 -0
- package/src/components/value-list/value-list.css +108 -0
- package/src/components/value-list/value-list.js +336 -0
- package/src/core/breakpoint.js +150 -0
- package/src/core/component.js +292 -0
- package/src/core/date.js +216 -0
- package/src/core/dom.js +260 -0
- package/src/core/export.js +204 -0
- package/src/core/fontawesome.js +177 -0
- package/src/core/format.js +283 -0
- package/src/core/http.js +246 -0
- package/src/core/i18n.js +83 -0
- package/src/core/icons.js +283 -0
- package/src/core/keyboard.js +240 -0
- package/src/core/position.js +177 -0
- package/src/core/storage.js +175 -0
- package/src/core/util.js +336 -0
- package/src/elements/define.js +715 -0
- package/src/elements/reflect.js +79 -0
- package/src/index.js +131 -0
- package/src/zeyos/connect.js +142 -0
- package/src/zeyos/form.js +254 -0
- package/src/zeyos/icons.js +92 -0
- package/src/zeyos/index.js +21 -0
- package/src/zeyos/modules.js +269 -0
- package/src/zeyos/query.js +199 -0
- package/src/zeyos/schema.js +474 -0
- package/src/zeyos/select.js +175 -0
- package/src/zeyos/table.js +272 -0
- package/styles/base.css +124 -0
- package/styles/icons.css +34 -0
- package/styles/tokens/dark.css +84 -0
- package/styles/tokens/density.css +9 -0
- package/styles/tokens/global.css +80 -0
- package/styles/tokens/modules.css +109 -0
- package/styles/tokens/semantic.css +49 -0
- package/styles/zx.css +61 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { Component } from '../../core/component.js';
|
|
2
|
+
import { h } from '../../core/dom.js';
|
|
3
|
+
import { icon } from '../../core/icons.js';
|
|
4
|
+
import { uid } from '../../core/util.js';
|
|
5
|
+
|
|
6
|
+
/** @typedef {Node|Component|(() => Node|Component)} TabContent */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} TabDefinition
|
|
10
|
+
* @property {string} name Stable tab name.
|
|
11
|
+
* @property {string} title Visible tab label.
|
|
12
|
+
* @property {TabContent} content Panel content or a lazy content factory.
|
|
13
|
+
* @property {string} [icon] Icon name passed to `icon()`, shown before the title.
|
|
14
|
+
* @property {boolean} [closable=false] Whether Delete can close the tab.
|
|
15
|
+
* @property {boolean} [disabled=false] Whether the tab is unavailable.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** @typedef {'divided'|'bracket'|'line'|'segmented'} TabboxVariant */
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Tab row appearances. All four are square-cornered; the boxed ones read the
|
|
22
|
+
* `--zx-tabbox-radius` custom property if an application wants rounding back.
|
|
23
|
+
*/
|
|
24
|
+
const VARIANTS = new Set(['divided', 'bracket', 'line', 'segmented']);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} TabInsertOptions
|
|
28
|
+
* @property {number} [index] Zero-based insertion index; omitted appends.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} TabboxOptions
|
|
33
|
+
* @property {TabDefinition[]} [tabs=[]] Initial tabs.
|
|
34
|
+
* @property {string|null} [active=null] Initially active tab name, or the first enabled tab.
|
|
35
|
+
* @property {TabboxVariant} [variant='divided'] `divided` sets flat blocks on a muted track above
|
|
36
|
+
* a bordered panel; `bracket` outlines folder tabs that fuse into that panel; `line` underlines
|
|
37
|
+
* the active tab across a full-width rule; `segmented` renders a compact group that reads as one
|
|
38
|
+
* control, for a toolbar or card header.
|
|
39
|
+
* @property {boolean} [keepAlive=true] Keep inactive panel elements mounted.
|
|
40
|
+
* @property {(event: CustomEvent<{name: string, previous: string|null}>) => void} [onchange] Change callback.
|
|
41
|
+
* @property {(event: CustomEvent<{name: string}>) => void} [onclose] Close callback.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} TabRecord
|
|
46
|
+
* @property {TabDefinition} definition Normalized definition.
|
|
47
|
+
* @property {HTMLButtonElement} tab Tab element.
|
|
48
|
+
* @property {HTMLElement} title Title element.
|
|
49
|
+
* @property {HTMLElement} badge Badge element.
|
|
50
|
+
* @property {HTMLElement} panel Tab panel.
|
|
51
|
+
* @property {boolean} built Whether content has been mounted.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* APG tabs with roving focus and manual activation.
|
|
56
|
+
* @fires Tabbox#change
|
|
57
|
+
* @fires Tabbox#close
|
|
58
|
+
* @extends {Component<TabboxOptions>}
|
|
59
|
+
*/
|
|
60
|
+
export class Tabbox extends Component {
|
|
61
|
+
static cssName = 'tabbox';
|
|
62
|
+
|
|
63
|
+
/** @type {Readonly<TabboxOptions>} */
|
|
64
|
+
static defaults = {
|
|
65
|
+
tabs: [],
|
|
66
|
+
active: null,
|
|
67
|
+
variant: 'divided',
|
|
68
|
+
keepAlive: true
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates or enhances a tabbox.
|
|
73
|
+
* @param {Element|string|null} target Existing container, selector, or null.
|
|
74
|
+
* @param {TabboxOptions} [options={}] Tabbox options.
|
|
75
|
+
*/
|
|
76
|
+
constructor(target, options = {}) {
|
|
77
|
+
super(target, options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @returns {HTMLElement} */
|
|
81
|
+
render() {
|
|
82
|
+
const created = !this.el;
|
|
83
|
+
const root = /** @type {HTMLElement} */ (this.el ?? h('section'));
|
|
84
|
+
this.el = root;
|
|
85
|
+
this._createdRoot = created;
|
|
86
|
+
this._original = created ? null : snapshot(root);
|
|
87
|
+
this._cleaned = false;
|
|
88
|
+
/** @type {TabRecord[]} */
|
|
89
|
+
this._tabs = [];
|
|
90
|
+
/** @type {TabRecord|null} */
|
|
91
|
+
this._active = null;
|
|
92
|
+
this._keepAlive = Boolean(this.options.keepAlive);
|
|
93
|
+
this._initializing = true;
|
|
94
|
+
if (!VARIANTS.has(this.options.variant)) {
|
|
95
|
+
throw new RangeError(`Unknown tabbox variant: ${this.options.variant}`);
|
|
96
|
+
}
|
|
97
|
+
root.dataset.variant = this.options.variant;
|
|
98
|
+
|
|
99
|
+
const tablist = h('div', {
|
|
100
|
+
class: 'zx-tabbox__tablist',
|
|
101
|
+
ref: 'tablist',
|
|
102
|
+
role: 'tablist',
|
|
103
|
+
ariaOrientation: 'horizontal'
|
|
104
|
+
});
|
|
105
|
+
const panels = h('div', {
|
|
106
|
+
class: 'zx-tabbox__panels',
|
|
107
|
+
ref: 'panels'
|
|
108
|
+
});
|
|
109
|
+
root.replaceChildren(tablist, panels);
|
|
110
|
+
|
|
111
|
+
if (!Array.isArray(this.options.tabs)) throw new TypeError('Tabbox tabs must be an array');
|
|
112
|
+
for (const definition of this.options.tabs) this.addTab(definition);
|
|
113
|
+
this._initializing = false;
|
|
114
|
+
|
|
115
|
+
const requested = this.options.active === null ? null : this._find(this.options.active);
|
|
116
|
+
const initial = requested && !requested.definition.disabled ? requested : this._enabledTabs()[0];
|
|
117
|
+
if (initial) this._applyActive(initial);
|
|
118
|
+
|
|
119
|
+
this.listen(tablist, 'click', (event) => {
|
|
120
|
+
const tab = event.target.closest?.('[role="tab"]');
|
|
121
|
+
if (!tab || !tablist.contains(tab)) return;
|
|
122
|
+
const record = this._recordForTab(tab);
|
|
123
|
+
if (!record || record.definition.disabled) return;
|
|
124
|
+
this._setFocused(record);
|
|
125
|
+
this.openTab(record.definition.name);
|
|
126
|
+
});
|
|
127
|
+
this.listen(tablist, 'keydown', (event) => this._onKeydown(event));
|
|
128
|
+
return root;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Adds a tab.
|
|
133
|
+
* @param {TabDefinition} definition Tab definition.
|
|
134
|
+
* @param {TabInsertOptions} [insert={}] Insertion options.
|
|
135
|
+
* @returns {this}
|
|
136
|
+
*/
|
|
137
|
+
addTab(definition, insert = {}) {
|
|
138
|
+
if (!definition || typeof definition !== 'object' || Array.isArray(definition)) {
|
|
139
|
+
throw new TypeError('Tab definition must be an object');
|
|
140
|
+
}
|
|
141
|
+
if (typeof definition.name !== 'string' || definition.name === '') {
|
|
142
|
+
throw new TypeError('Tab name must be a non-empty string');
|
|
143
|
+
}
|
|
144
|
+
if (this._find(definition.name)) throw new RangeError(`Tab already exists: ${definition.name}`);
|
|
145
|
+
if (!isTabContent(definition.content)) {
|
|
146
|
+
throw new TypeError('Tab content must be a Node, Component, or function');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const normalized = {
|
|
150
|
+
name: definition.name,
|
|
151
|
+
title: String(definition.title ?? ''),
|
|
152
|
+
content: definition.content,
|
|
153
|
+
icon: definition.icon ? String(definition.icon) : null,
|
|
154
|
+
closable: Boolean(definition.closable),
|
|
155
|
+
disabled: Boolean(definition.disabled)
|
|
156
|
+
};
|
|
157
|
+
const tabId = uid('zx-tab');
|
|
158
|
+
const panelId = uid('zx-tabpanel');
|
|
159
|
+
const title = h('span', { class: 'zx-tabbox__title' }, normalized.title);
|
|
160
|
+
const badge = h('span', {
|
|
161
|
+
class: 'zx-tabbox__badge',
|
|
162
|
+
hidden: true
|
|
163
|
+
});
|
|
164
|
+
const children = [];
|
|
165
|
+
if (normalized.icon) {
|
|
166
|
+
children.push(h('span', { class: 'zx-tabbox__icon', ariaHidden: 'true' },
|
|
167
|
+
icon(normalized.icon, { size: 16 })));
|
|
168
|
+
}
|
|
169
|
+
children.push(title, badge);
|
|
170
|
+
if (normalized.closable) {
|
|
171
|
+
children.push(h('span', {
|
|
172
|
+
class: 'zx-tabbox__close',
|
|
173
|
+
ariaHidden: 'true'
|
|
174
|
+
}, icon('x', { size: 14 })));
|
|
175
|
+
}
|
|
176
|
+
const tab = /** @type {HTMLButtonElement} */ (h('button', {
|
|
177
|
+
class: 'zx-tabbox__tab',
|
|
178
|
+
type: 'button',
|
|
179
|
+
id: tabId,
|
|
180
|
+
role: 'tab',
|
|
181
|
+
tabindex: '-1',
|
|
182
|
+
ariaControls: panelId,
|
|
183
|
+
ariaSelected: 'false',
|
|
184
|
+
ariaDisabled: normalized.disabled ? 'true' : null,
|
|
185
|
+
dataset: { closable: normalized.closable ? 'true' : null }
|
|
186
|
+
}, children));
|
|
187
|
+
const panel = h('div', {
|
|
188
|
+
class: 'zx-tabbox__panel',
|
|
189
|
+
id: panelId,
|
|
190
|
+
role: 'tabpanel',
|
|
191
|
+
tabindex: '0',
|
|
192
|
+
ariaLabelledby: tabId,
|
|
193
|
+
hidden: true
|
|
194
|
+
});
|
|
195
|
+
/** @type {TabRecord} */
|
|
196
|
+
const record = { definition: normalized, tab, title, badge, panel, built: false };
|
|
197
|
+
const numericIndex = Number.isFinite(insert.index) ? Math.trunc(insert.index) : this._tabs.length;
|
|
198
|
+
const index = Math.max(0, Math.min(this._tabs.length, numericIndex));
|
|
199
|
+
const before = this._tabs[index] ?? null;
|
|
200
|
+
this._tabs.splice(index, 0, record);
|
|
201
|
+
this.refs.tablist.insertBefore(tab, before?.tab ?? null);
|
|
202
|
+
if (this._keepAlive) this.refs.panels.insertBefore(panel, before?.panel ?? null);
|
|
203
|
+
|
|
204
|
+
if (!this._initializing && !this._active && !normalized.disabled) this._applyActive(record);
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Removes a tab by name.
|
|
210
|
+
* @param {string} name Tab name.
|
|
211
|
+
* @returns {this}
|
|
212
|
+
*/
|
|
213
|
+
removeTab(name) {
|
|
214
|
+
this._removeTab(name, false);
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Activates an enabled tab unless its change event is vetoed.
|
|
220
|
+
* @param {string} name Tab name.
|
|
221
|
+
* @returns {this}
|
|
222
|
+
* @fires Tabbox#change
|
|
223
|
+
*/
|
|
224
|
+
openTab(name) {
|
|
225
|
+
const record = this._find(name);
|
|
226
|
+
if (!record || record.definition.disabled || record === this._active) return this;
|
|
227
|
+
const previous = this._active?.definition.name ?? null;
|
|
228
|
+
const event = this.emit('change', { name: record.definition.name, previous });
|
|
229
|
+
if (event.defaultPrevented) return this;
|
|
230
|
+
this._applyActive(record);
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Alias of `openTab()`.
|
|
236
|
+
* @param {string} name Tab name.
|
|
237
|
+
* @returns {this}
|
|
238
|
+
* @fires Tabbox#change
|
|
239
|
+
*/
|
|
240
|
+
show(name) {
|
|
241
|
+
return this.openTab(name);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Returns the active tab name.
|
|
246
|
+
* @returns {string|null}
|
|
247
|
+
*/
|
|
248
|
+
getActive() {
|
|
249
|
+
return this._active?.definition.name ?? null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Changes a tab title.
|
|
254
|
+
* @param {string} name Tab name.
|
|
255
|
+
* @param {string} title Next title.
|
|
256
|
+
* @returns {this}
|
|
257
|
+
*/
|
|
258
|
+
setTitle(name, title) {
|
|
259
|
+
const record = this._require(name);
|
|
260
|
+
record.definition.title = String(title ?? '');
|
|
261
|
+
record.title.textContent = record.definition.title;
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Sets or clears a tab badge.
|
|
267
|
+
* @param {string} name Tab name.
|
|
268
|
+
* @param {string|null} text Badge text, or null to remove it.
|
|
269
|
+
* @returns {this}
|
|
270
|
+
*/
|
|
271
|
+
setBadge(name, text) {
|
|
272
|
+
const record = this._require(name);
|
|
273
|
+
record.badge.textContent = text === null ? '' : String(text);
|
|
274
|
+
record.badge.hidden = text === null;
|
|
275
|
+
return this;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Enables a tab.
|
|
280
|
+
* @param {string} name Tab name.
|
|
281
|
+
* @returns {this}
|
|
282
|
+
*/
|
|
283
|
+
enableTab(name) {
|
|
284
|
+
const record = this._require(name);
|
|
285
|
+
if (!record.definition.disabled) return this;
|
|
286
|
+
record.definition.disabled = false;
|
|
287
|
+
record.tab.removeAttribute('aria-disabled');
|
|
288
|
+
if (!this._active) this._applyActive(record);
|
|
289
|
+
return this;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Disables a tab, moving selection when it is active.
|
|
294
|
+
* @param {string} name Tab name.
|
|
295
|
+
* @returns {this}
|
|
296
|
+
* @fires Tabbox#change
|
|
297
|
+
*/
|
|
298
|
+
disableTab(name) {
|
|
299
|
+
const record = this._require(name);
|
|
300
|
+
if (record.definition.disabled) return this;
|
|
301
|
+
if (record === this._active) {
|
|
302
|
+
const replacement = this._enabledTabs().find((item) => item !== record) ?? null;
|
|
303
|
+
if (replacement) {
|
|
304
|
+
this.openTab(replacement.definition.name);
|
|
305
|
+
if (this._active === record) return this;
|
|
306
|
+
} else {
|
|
307
|
+
record.tab.setAttribute('aria-selected', 'false');
|
|
308
|
+
record.tab.tabIndex = -1;
|
|
309
|
+
record.panel.hidden = true;
|
|
310
|
+
if (!this._keepAlive) record.panel.remove();
|
|
311
|
+
this._active = null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
record.definition.disabled = true;
|
|
315
|
+
record.tab.setAttribute('aria-disabled', 'true');
|
|
316
|
+
record.tab.tabIndex = -1;
|
|
317
|
+
return this;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** @returns {void} */
|
|
321
|
+
destroy() {
|
|
322
|
+
if (this._cleaned) return;
|
|
323
|
+
this._cleaned = true;
|
|
324
|
+
super.destroy();
|
|
325
|
+
if (!this._createdRoot && this._original) restore(this.el, this._original);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** @param {KeyboardEvent} event @returns {void} */
|
|
329
|
+
_onKeydown(event) {
|
|
330
|
+
const tab = event.target.closest?.('[role="tab"]');
|
|
331
|
+
if (!tab || !this.refs.tablist.contains(tab)) return;
|
|
332
|
+
const current = this._recordForTab(tab);
|
|
333
|
+
if (!current) return;
|
|
334
|
+
const enabled = this._enabledTabs();
|
|
335
|
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
|
336
|
+
event.preventDefault();
|
|
337
|
+
const offset = event.key === 'ArrowRight' ? 1 : -1;
|
|
338
|
+
const index = enabled.indexOf(current);
|
|
339
|
+
const next = enabled[(index + offset + enabled.length) % enabled.length];
|
|
340
|
+
if (next) this._setFocused(next);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (event.key === 'Home' || event.key === 'End') {
|
|
344
|
+
event.preventDefault();
|
|
345
|
+
const next = event.key === 'Home' ? enabled[0] : enabled[enabled.length - 1];
|
|
346
|
+
if (next) this._setFocused(next);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
350
|
+
event.preventDefault();
|
|
351
|
+
this.openTab(current.definition.name);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (event.key === 'Delete' && current.definition.closable) {
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
const index = this._tabs.indexOf(current);
|
|
357
|
+
if (!this._removeTab(current.definition.name, true)) return;
|
|
358
|
+
const remaining = this._tabs[Math.min(index, this._tabs.length - 1)] ?? this._tabs[index - 1];
|
|
359
|
+
const focusTarget = remaining && !remaining.definition.disabled ? remaining : this._active;
|
|
360
|
+
if (focusTarget) this._setFocused(focusTarget);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** @param {string} name @param {boolean} emitClose @returns {boolean} */
|
|
365
|
+
_removeTab(name, emitClose) {
|
|
366
|
+
const record = this._find(name);
|
|
367
|
+
if (!record) return false;
|
|
368
|
+
const index = this._tabs.indexOf(record);
|
|
369
|
+
const remaining = this._tabs.filter((item) => item !== record);
|
|
370
|
+
const enabled = remaining.filter((item) => !item.definition.disabled);
|
|
371
|
+
const replacement = enabled.find((item) => this._tabs.indexOf(item) > index) ?? enabled.at(-1) ?? null;
|
|
372
|
+
if (record === this._active && replacement) {
|
|
373
|
+
const event = this.emit('change', {
|
|
374
|
+
name: replacement.definition.name,
|
|
375
|
+
previous: record.definition.name
|
|
376
|
+
});
|
|
377
|
+
if (event.defaultPrevented) return false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this._tabs.splice(index, 1);
|
|
381
|
+
record.tab.remove();
|
|
382
|
+
record.panel.remove();
|
|
383
|
+
if (record === this._active) {
|
|
384
|
+
this._active = null;
|
|
385
|
+
if (replacement) this._applyActive(replacement);
|
|
386
|
+
}
|
|
387
|
+
if (emitClose) this.emit('close', { name: record.definition.name });
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** @param {TabRecord} record @returns {void} */
|
|
392
|
+
_applyActive(record) {
|
|
393
|
+
this._ensureContent(record);
|
|
394
|
+
const previous = this._active;
|
|
395
|
+
if (previous && previous !== record) {
|
|
396
|
+
previous.tab.setAttribute('aria-selected', 'false');
|
|
397
|
+
previous.panel.hidden = true;
|
|
398
|
+
if (!this._keepAlive) previous.panel.remove();
|
|
399
|
+
}
|
|
400
|
+
this._active = record;
|
|
401
|
+
record.tab.setAttribute('aria-selected', 'true');
|
|
402
|
+
record.panel.hidden = false;
|
|
403
|
+
if (record.panel.parentNode !== this.refs.panels) this.refs.panels.append(record.panel);
|
|
404
|
+
this._setFocused(record, false);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** @param {TabRecord} record @returns {void} */
|
|
408
|
+
_ensureContent(record) {
|
|
409
|
+
if (record.built) return;
|
|
410
|
+
const content = typeof record.definition.content === 'function' ?
|
|
411
|
+
record.definition.content() : record.definition.content;
|
|
412
|
+
const node = content instanceof Component ? content.toElement() : content;
|
|
413
|
+
if (!node || typeof node.nodeType !== 'number') {
|
|
414
|
+
throw new TypeError(`Tab content factory for ${record.definition.name} must return a Node or Component`);
|
|
415
|
+
}
|
|
416
|
+
record.panel.append(node);
|
|
417
|
+
record.built = true;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** @param {TabRecord} record @param {boolean} [focus=true] @returns {void} */
|
|
421
|
+
_setFocused(record, focus = true) {
|
|
422
|
+
for (const item of this._tabs) item.tab.tabIndex = item === record ? 0 : -1;
|
|
423
|
+
if (focus) record.tab.focus();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** @returns {TabRecord[]} */
|
|
427
|
+
_enabledTabs() {
|
|
428
|
+
return this._tabs.filter((record) => !record.definition.disabled);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** @param {string} name @returns {TabRecord|null} */
|
|
432
|
+
_find(name) {
|
|
433
|
+
return this._tabs.find((record) => record.definition.name === name) ?? null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** @param {string} name @returns {TabRecord} */
|
|
437
|
+
_require(name) {
|
|
438
|
+
const record = this._find(name);
|
|
439
|
+
if (!record) throw new RangeError(`Unknown tab: ${name}`);
|
|
440
|
+
return record;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** @param {Element} tab @returns {TabRecord|null} */
|
|
444
|
+
_recordForTab(tab) {
|
|
445
|
+
return this._tabs.find((record) => record.tab === tab) ?? null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Fired before the active tab changes. Calling `preventDefault()` vetoes the switch.
|
|
451
|
+
* @event Tabbox#change
|
|
452
|
+
* @type {CustomEvent<{name: string, previous: string|null}>}
|
|
453
|
+
*/
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Fired after a closable tab is removed through the keyboard.
|
|
457
|
+
* @event Tabbox#close
|
|
458
|
+
* @type {CustomEvent<{name: string}>}
|
|
459
|
+
*/
|
|
460
|
+
|
|
461
|
+
/** @param {unknown} content @returns {content is TabContent} */
|
|
462
|
+
function isTabContent(content) {
|
|
463
|
+
if (typeof content === 'function' || content instanceof Component) return true;
|
|
464
|
+
return Boolean(content && typeof content === 'object' && typeof content.nodeType === 'number');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** @param {Element} element @returns {{attributes: Array<[string, string]>, children: Node[]}} */
|
|
468
|
+
function snapshot(element) {
|
|
469
|
+
return {
|
|
470
|
+
attributes: Array.from(element.attributes, (attribute) => [attribute.name, attribute.value]),
|
|
471
|
+
children: Array.from(element.childNodes)
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* @param {Element} element
|
|
477
|
+
* @param {{attributes: Array<[string, string]>, children: Node[]}} original
|
|
478
|
+
* @returns {void}
|
|
479
|
+
*/
|
|
480
|
+
function restore(element, original) {
|
|
481
|
+
for (const attribute of Array.from(element.attributes)) element.removeAttribute(attribute.name);
|
|
482
|
+
for (const [name, value] of original.attributes) element.setAttribute(name, value);
|
|
483
|
+
element.replaceChildren(...original.children);
|
|
484
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** @typedef {'asc'|'desc'} SortDirection */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compares strings with the host locale's collation rules.
|
|
5
|
+
* @param {unknown} left Left value.
|
|
6
|
+
* @param {unknown} right Right value.
|
|
7
|
+
* @returns {number} Negative, zero, or positive comparison result.
|
|
8
|
+
*/
|
|
9
|
+
export function compareStrings(left, right) {
|
|
10
|
+
return String(left).localeCompare(String(right));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compares numeric values.
|
|
15
|
+
* @param {number} left Left value.
|
|
16
|
+
* @param {number} right Right value.
|
|
17
|
+
* @returns {number} Negative, zero, or positive comparison result.
|
|
18
|
+
*/
|
|
19
|
+
export function compareNumbers(left, right) {
|
|
20
|
+
return left - right;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Compares Date values by their timestamps.
|
|
25
|
+
* @param {Date} left Left date.
|
|
26
|
+
* @param {Date} right Right date.
|
|
27
|
+
* @returns {number} Negative, zero, or positive comparison result.
|
|
28
|
+
*/
|
|
29
|
+
export function compareDates(left, right) {
|
|
30
|
+
return left.getTime() - right.getTime();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compares values according to their runtime type. Nullish values are always placed last,
|
|
35
|
+
* including for descending sorts.
|
|
36
|
+
* @param {unknown} left Left value.
|
|
37
|
+
* @param {unknown} right Right value.
|
|
38
|
+
* @param {SortDirection} [direction='asc'] Sort direction.
|
|
39
|
+
* @returns {number} Negative, zero, or positive comparison result.
|
|
40
|
+
*/
|
|
41
|
+
export function compareValues(left, right, direction = 'asc') {
|
|
42
|
+
const leftNull = left == null;
|
|
43
|
+
const rightNull = right == null;
|
|
44
|
+
if (leftNull || rightNull) {
|
|
45
|
+
if (leftNull && rightNull) return 0;
|
|
46
|
+
return leftNull ? 1 : -1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let result;
|
|
50
|
+
if (left instanceof Date && right instanceof Date) {
|
|
51
|
+
result = compareDates(left, right);
|
|
52
|
+
} else if (typeof left === 'number' && typeof right === 'number') {
|
|
53
|
+
result = compareNumbers(left, right);
|
|
54
|
+
} else {
|
|
55
|
+
result = compareStrings(left, right);
|
|
56
|
+
}
|
|
57
|
+
return direction === 'desc' ? -result : result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Creates a row comparator for a sort-value accessor.
|
|
62
|
+
* @template T
|
|
63
|
+
* @param {(row: T) => unknown} getValue Sort-value accessor.
|
|
64
|
+
* @param {SortDirection} [direction='asc'] Sort direction.
|
|
65
|
+
* @returns {(left: T, right: T) => number} Row comparator.
|
|
66
|
+
*/
|
|
67
|
+
export function createComparator(getValue, direction = 'asc') {
|
|
68
|
+
return (left, right) => compareValues(getValue(left), getValue(right), direction);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns a stably sorted copy of an array without mutating the input.
|
|
73
|
+
* @template T
|
|
74
|
+
* @param {T[]} rows Rows to sort.
|
|
75
|
+
* @param {(row: T) => unknown} getValue Sort-value accessor.
|
|
76
|
+
* @param {SortDirection} [direction='asc'] Sort direction.
|
|
77
|
+
* @returns {T[]} Sorted copy.
|
|
78
|
+
*/
|
|
79
|
+
export function sortRows(rows, getValue, direction = 'asc') {
|
|
80
|
+
const compare = createComparator(getValue, direction);
|
|
81
|
+
return rows
|
|
82
|
+
.map((row, index) => ({ row, index }))
|
|
83
|
+
.sort((left, right) => compare(left.row, right.row) || left.index - right.index)
|
|
84
|
+
.map(({ row }) => row);
|
|
85
|
+
}
|