@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
|
@@ -6,23 +6,61 @@ import { uid } from '../../core/util.js';
|
|
|
6
6
|
import {
|
|
7
7
|
AppRailPresenter as MinimizedPresenter, appNavIcon, normalizeAppItems
|
|
8
8
|
} from '../../internal/app-rail.js';
|
|
9
|
+
import { ModuleBar, liftSections, moduleOf } from '../module-bar/module-bar.js';
|
|
10
|
+
import { Tooltip } from '../tooltip/tooltip.js';
|
|
11
|
+
|
|
12
|
+
/** The modules layout's presentations, widest first. */
|
|
13
|
+
const PRESENTATIONS = Object.freeze(['expanded', 'compact', 'collapsed']);
|
|
9
14
|
|
|
10
15
|
/** @typedef {import('../../internal/app-rail.js').AppNavItem} AppNavItem */
|
|
11
16
|
/** @typedef {import('../../internal/app-rail.js').AppNavSelectDetail} AppNavSelectDetail */
|
|
17
|
+
/** @typedef {import('../module-bar/module-bar.js').ModuleItem} ModuleItem */
|
|
18
|
+
/**
|
|
19
|
+
* A module in the modules layout: a `ModuleItem` whose panel may carry content of its own.
|
|
20
|
+
* @typedef {ModuleItem & {header?: Node|{toElement: () => Node|null}, content?: Node|{toElement: () => Node|null}}} AppSidebarModule
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Presentation of the module strip in the modules layout.
|
|
24
|
+
* @typedef {Object} AppSidebarModuleBarOptions
|
|
25
|
+
* @property {'tint'|'solid'|'plain'} [appearance='tint'] Tile treatment.
|
|
26
|
+
* @property {'auto'|'beside'|'below'|'hidden'} [labels='auto'] Module names on the strip: `'auto'`
|
|
27
|
+
* prints them beside the tiles while expanded and leaves the icons alone while collapsed;
|
|
28
|
+
* `'hidden'` keeps the icon strip beside the panel; `'below'` prints them under the tiles.
|
|
29
|
+
* @property {'auto'|'always'|'never'} [allModules='auto'] The "All modules" button.
|
|
30
|
+
* @property {string|null} [label=null] Strip accessible label; omitted resolves `moduleBar.label`.
|
|
31
|
+
*/
|
|
12
32
|
/**
|
|
13
33
|
* @typedef {Object} AppSidebarOptions
|
|
14
|
-
* @property {AppNavItem[]} [items=[]] Application navigation tree
|
|
34
|
+
* @property {AppNavItem[]|AppSidebarModule[]} [items=[]] Application navigation tree; in the
|
|
35
|
+
* modules layout its top level is the modules.
|
|
15
36
|
* @property {string|number|null} [active=null] Active item id.
|
|
16
37
|
* @property {Array<string|number>} [expanded=[]] Initially expanded branch ids.
|
|
17
38
|
* @property {boolean} [collapsed=false] Whether the minimized presentation is shown.
|
|
18
39
|
* @property {boolean} [collapsible=true] Whether vertical navigation exposes a mode toggle.
|
|
19
40
|
* @property {'vertical'|'horizontal'} [orientation='vertical'] Navigation flow; horizontal is always minimized.
|
|
20
41
|
* @property {'left'|'right'|'top'|'bottom'} [side='left'] Workspace edge occupied by navigation.
|
|
42
|
+
* @property {'tree'|'modules'} [layout='tree'] `'modules'` draws the top level as a module strip
|
|
43
|
+
* beside a panel showing one module's menu; minimized, the strip remains and menus open as flyouts.
|
|
44
|
+
* @property {string|number|null} [module=null] Modules layout: the module the panel shows first.
|
|
45
|
+
* Defaults to the module containing `active`, else the first module with a panel.
|
|
46
|
+
* @property {AppSidebarModuleBarOptions} [moduleBar={}] Modules layout: strip presentation.
|
|
47
|
+
* @property {'expanded'|'compact'|'collapsed'|null} [presentation=null] Modules layout: how much
|
|
48
|
+
* of the navigation shows — both menus expanded (module names beside the side menu), the module
|
|
49
|
+
* menu collapsed to icons beside the side menu, or both collapsed to the icon strip. Defaults
|
|
50
|
+
* to `collapsed` ? `'collapsed'` : `'expanded'`.
|
|
51
|
+
* @property {Array<'expanded'|'compact'|'collapsed'>} [presentations=['expanded','compact','collapsed']]
|
|
52
|
+
* Modules layout: the presentations offered, widest first.
|
|
53
|
+
* @property {boolean} [presentationSwitch=true] Modules layout: whether the strip's foot carries the
|
|
54
|
+
* switch between presentations. Turn it off when the choice lives elsewhere — an account menu,
|
|
55
|
+
* through `presentationChoice()`.
|
|
21
56
|
* @property {string} [label='Applications'] Navigation accessible label.
|
|
22
|
-
* @property {Node|string|number|{toElement: () => Node|null}|null} [header=null] Expanded header content
|
|
57
|
+
* @property {Node|string|number|{toElement: () => Node|null}|null} [header=null] Expanded header content;
|
|
58
|
+
* in the modules layout, content above every module's menu.
|
|
23
59
|
* @property {Node|string|number|{toElement: () => Node|null}|null} [footer=null] Expanded sticky footer content.
|
|
24
|
-
* @property {Node|string|number|{toElement: () => Node|null}|null} [railHeader=null] Minimized leading content
|
|
25
|
-
*
|
|
60
|
+
* @property {Node|string|number|{toElement: () => Node|null}|null} [railHeader=null] Minimized leading content;
|
|
61
|
+
* in the modules layout, the strip's leading content in both states.
|
|
62
|
+
* @property {Node|string|number|{toElement: () => Node|null}|null} [railFooter=null] Minimized trailing content;
|
|
63
|
+
* in the modules layout, the strip's trailing content in both states.
|
|
26
64
|
* @property {number} [openDelay=80] Minimized flyout hover-open delay.
|
|
27
65
|
* @property {number} [closeDelay=160] Minimized flyout crossing grace.
|
|
28
66
|
* @property {(item: AppNavItem, context: Record<string, any>) => Node|null} [renderIcon] Optional host icon renderer.
|
|
@@ -30,15 +68,20 @@ import {
|
|
|
30
68
|
* @property {(event: CustomEvent<{item: AppNavItem|null, id: string|number|null, expanded: boolean|null, ids: Array<string|number>}>) => void} [onbranchchange] Branch-state listener.
|
|
31
69
|
* @property {(event: CustomEvent<{collapsed: boolean}>) => void} [oncollapsechange] Presentation-state listener.
|
|
32
70
|
* @property {(event: CustomEvent<{item: AppNavItem, id: string|number, open: boolean}>) => void} [onflyoutchange] Minimized flyout-state listener.
|
|
71
|
+
* @property {(event: CustomEvent<{item: AppNavItem, id: string|number, previous: string|number|null}>) => void} [onmodulechange] Preventable module-switch listener.
|
|
72
|
+
* @property {(event: CustomEvent<{presentation: 'expanded'|'compact'|'collapsed', previous: 'expanded'|'compact'|'collapsed'}>) => void} [onpresentationchange] Presentation listener.
|
|
33
73
|
*/
|
|
34
74
|
|
|
35
75
|
/**
|
|
36
76
|
* One application navigation component. Vertical expanded descendants render inline; minimized
|
|
37
|
-
* vertical and horizontal descendants use pointer-, focus-, and keyboard-accessible flyouts.
|
|
77
|
+
* vertical and horizontal descendants use pointer-, focus-, and keyboard-accessible flyouts. The
|
|
78
|
+
* modules layout puts a `ModuleBar` of the top level beside a panel with the current module's menu.
|
|
38
79
|
* @fires AppSidebar#select
|
|
39
80
|
* @fires AppSidebar#branchchange
|
|
40
81
|
* @fires AppSidebar#collapsechange
|
|
41
82
|
* @fires AppSidebar#flyoutchange
|
|
83
|
+
* @fires AppSidebar#modulechange
|
|
84
|
+
* @fires AppSidebar#presentationchange
|
|
42
85
|
* @extends {Component<AppSidebarOptions>}
|
|
43
86
|
*/
|
|
44
87
|
export class AppSidebar extends Component {
|
|
@@ -53,6 +96,12 @@ export class AppSidebar extends Component {
|
|
|
53
96
|
collapsible: true,
|
|
54
97
|
orientation: 'vertical',
|
|
55
98
|
side: 'left',
|
|
99
|
+
layout: 'tree',
|
|
100
|
+
module: null,
|
|
101
|
+
moduleBar: {},
|
|
102
|
+
presentation: null,
|
|
103
|
+
presentations: ['expanded', 'compact', 'collapsed'],
|
|
104
|
+
presentationSwitch: true,
|
|
56
105
|
label: 'Applications',
|
|
57
106
|
header: null,
|
|
58
107
|
footer: null,
|
|
@@ -93,6 +142,16 @@ export class AppSidebar extends Component {
|
|
|
93
142
|
this._collapsed = Boolean(this.options.collapsed);
|
|
94
143
|
this._orientation = this.options.orientation === 'horizontal' ? 'horizontal' : 'vertical';
|
|
95
144
|
this._side = normalizeSide(this._orientation, this.options.side);
|
|
145
|
+
this._modules = this.options.layout === 'modules';
|
|
146
|
+
this._moduleLabels = this.options.moduleBar?.labels ?? 'auto';
|
|
147
|
+
this._compact = false;
|
|
148
|
+
this._presentationGroup = null;
|
|
149
|
+
this._stripTooltips = [];
|
|
150
|
+
if (this._modules && PRESENTATIONS.includes(this.options.presentation)) {
|
|
151
|
+
this._collapsed = this.options.presentation === 'collapsed';
|
|
152
|
+
this._compact = this.options.presentation === 'compact';
|
|
153
|
+
}
|
|
154
|
+
this._shownId = this._modules ? this._resolveShown(this.options.module) : null;
|
|
96
155
|
this._modeAbort = null;
|
|
97
156
|
root.replaceChildren();
|
|
98
157
|
return root;
|
|
@@ -102,7 +161,12 @@ export class AppSidebar extends Component {
|
|
|
102
161
|
setItems(items) {
|
|
103
162
|
this._items = normalizeAppItems(items);
|
|
104
163
|
this._reconcileExpanded();
|
|
105
|
-
if (this.
|
|
164
|
+
if (this._modules) {
|
|
165
|
+
this._shownId = this._resolveShown(this._shownId);
|
|
166
|
+
this._rail?.setItems(this._items);
|
|
167
|
+
if (this._rail instanceof ModuleBar && !this.isCollapsed()) this._rail.setExpanded(this._shownId);
|
|
168
|
+
if (this.refs.list) this._renderPanelContent();
|
|
169
|
+
} else if (this._rail) {
|
|
106
170
|
this._rail.setItems(this._items);
|
|
107
171
|
this._decorateRail();
|
|
108
172
|
} else this._renderTree();
|
|
@@ -118,19 +182,110 @@ export class AppSidebar extends Component {
|
|
|
118
182
|
return copyItems(this._items);
|
|
119
183
|
}
|
|
120
184
|
|
|
121
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Sets the active id without routing. In the modules layout the panel follows the active route
|
|
187
|
+
* into its module.
|
|
188
|
+
* @param {string|number|null} id
|
|
189
|
+
* @returns {this}
|
|
190
|
+
*/
|
|
122
191
|
setActive(id) {
|
|
123
192
|
this._active = id;
|
|
124
193
|
if (this._rail) this._rail.setActive(id);
|
|
125
|
-
|
|
194
|
+
if (this._modules) {
|
|
195
|
+
const owner = moduleOf(this._moduleList(), id);
|
|
196
|
+
if (owner && isPanelModule(owner) && String(owner.id) !== String(this._shownId)) {
|
|
197
|
+
this._shownId = owner.id;
|
|
198
|
+
if (this._rail instanceof ModuleBar && !this.isCollapsed()) this._rail.setExpanded(owner.id);
|
|
199
|
+
if (this.refs.list) this._renderPanelContent();
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (this.refs.list) this._syncActive();
|
|
126
204
|
return this;
|
|
127
205
|
}
|
|
128
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Updates one badge — a module's, or any destination's — without rebuilding the navigation.
|
|
209
|
+
* @param {string|number} id Item id.
|
|
210
|
+
* @param {string|number|null} badge New badge; null removes it.
|
|
211
|
+
* @returns {this}
|
|
212
|
+
*/
|
|
213
|
+
setBadge(id, badge) {
|
|
214
|
+
const item = findItem(this._items, id);
|
|
215
|
+
if (!item) return this;
|
|
216
|
+
if (badge == null || badge === '') delete item.badge;
|
|
217
|
+
else item.badge = badge;
|
|
218
|
+
if (this._rail instanceof ModuleBar) this._rail.setBadge(id, badge);
|
|
219
|
+
else if (this._rail) {
|
|
220
|
+
this._rail.setItems(this._items);
|
|
221
|
+
this._decorateRail();
|
|
222
|
+
}
|
|
223
|
+
if (this.refs.list) this._renderTree();
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Modules layout: shows a module's menu in the panel. Fires a preventable `modulechange` unless silent.
|
|
229
|
+
* @param {string|number} id Module id.
|
|
230
|
+
* @param {{silent?: boolean}} [options={}]
|
|
231
|
+
* @returns {this}
|
|
232
|
+
*/
|
|
233
|
+
setModule(id, { silent = false } = {}) {
|
|
234
|
+
this._showModule(id, { silent });
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Modules layout: the module whose menu the panel shows. @returns {string|number|null} */
|
|
239
|
+
getModule() {
|
|
240
|
+
return this._shownId;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Modules layout: whether the strip prints module names — `'auto'` (beside the tiles while
|
|
245
|
+
* expanded, none while collapsed), `'beside'`, `'below'`, or `'hidden'`. A narrow drawer that
|
|
246
|
+
* has no room for a labelled strip beside the panel sets `'hidden'`.
|
|
247
|
+
* @param {'auto'|'beside'|'below'|'hidden'} labels
|
|
248
|
+
* @returns {this}
|
|
249
|
+
*/
|
|
250
|
+
setModuleLabels(labels) {
|
|
251
|
+
this._moduleLabels = labels;
|
|
252
|
+
if (this._rail instanceof ModuleBar) this._rail.setLabels(this._stripLabels());
|
|
253
|
+
this._syncPresentationSwitch();
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* How much of the navigation shows: `'expanded'` (in the modules layout, both menus),
|
|
259
|
+
* `'compact'` (the module menu collapsed to icons beside the side menu), or `'collapsed'`.
|
|
260
|
+
* @returns {'expanded'|'compact'|'collapsed'}
|
|
261
|
+
*/
|
|
262
|
+
getPresentation() {
|
|
263
|
+
if (this.isCollapsed()) return 'collapsed';
|
|
264
|
+
return this._modules && this._stripLabels() === 'hidden' ? 'compact' : 'expanded';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Shows the navigation as `'expanded'`, `'compact'` (modules layout), or `'collapsed'`.
|
|
269
|
+
* @param {'expanded'|'compact'|'collapsed'} presentation
|
|
270
|
+
* @param {{silent?: boolean}} [options={}]
|
|
271
|
+
* @returns {this}
|
|
272
|
+
*/
|
|
273
|
+
setPresentation(presentation, { silent = false } = {}) {
|
|
274
|
+
if (!PRESENTATIONS.includes(presentation)) return this;
|
|
275
|
+
return this._present(presentation === 'collapsed',
|
|
276
|
+
presentation === 'collapsed' ? this._compact : this._modules && presentation === 'compact', { silent });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Moves to the next presentation the strip's switch offers, wrapping (Mod+B in an application). @returns {this} */
|
|
280
|
+
togglePresentation() {
|
|
281
|
+
return this.setPresentation(this._nextPresentation());
|
|
282
|
+
}
|
|
283
|
+
|
|
129
284
|
/** Replaces expanded branch ids. @param {Array<string|number>} ids @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
130
285
|
setExpanded(ids, { silent = false } = {}) {
|
|
131
286
|
this._expanded = new Set(Array.isArray(ids) ? ids.map(String) : []);
|
|
132
287
|
this._reconcileExpanded();
|
|
133
|
-
if (
|
|
288
|
+
if (this.refs.list) this._renderTree();
|
|
134
289
|
if (!silent) this.emit('branchchange', {
|
|
135
290
|
item: null,
|
|
136
291
|
id: null,
|
|
@@ -152,12 +307,12 @@ export class AppSidebar extends Component {
|
|
|
152
307
|
/** Toggles one inline branch. @param {string|number} id @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
153
308
|
toggleBranch(id, { silent = false } = {}) {
|
|
154
309
|
const item = findItem(this._items, id);
|
|
155
|
-
if (!item?.children?.length) return this;
|
|
310
|
+
if (!item?.children?.length || item.section) return this;
|
|
156
311
|
const key = String(id);
|
|
157
312
|
const expanded = !this._expanded.has(key);
|
|
158
313
|
if (expanded) this._expanded.add(key);
|
|
159
314
|
else this._expanded.delete(key);
|
|
160
|
-
if (
|
|
315
|
+
if (this.refs.list) {
|
|
161
316
|
this._renderTree();
|
|
162
317
|
if (expanded) this._markBranchMotion(key);
|
|
163
318
|
}
|
|
@@ -182,19 +337,45 @@ export class AppSidebar extends Component {
|
|
|
182
337
|
|
|
183
338
|
/** Sets the saved vertical expanded/minimized state. @param {boolean} collapsed @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
184
339
|
setCollapsed(collapsed, { silent = false } = {}) {
|
|
185
|
-
|
|
340
|
+
return this._present(Boolean(collapsed), this._compact, { silent });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Applies a collapsed state and, in the modules layout, whether the module menu is collapsed
|
|
345
|
+
* beside an expanded side menu; reports what changed.
|
|
346
|
+
* @param {boolean} collapsed
|
|
347
|
+
* @param {boolean} compact
|
|
348
|
+
* @param {{silent?: boolean}} options
|
|
349
|
+
* @returns {this}
|
|
350
|
+
*/
|
|
351
|
+
_present(collapsed, compact, { silent = false }) {
|
|
352
|
+
const previous = { collapsed: this.isCollapsed(), presentation: this.getPresentation() };
|
|
186
353
|
const active = document.activeElement;
|
|
187
354
|
const outgoingOwnedFocus = Boolean(active
|
|
188
355
|
&& (this.el.contains(active) || this._rail?._containsNode(active)));
|
|
189
|
-
this._collapsed =
|
|
356
|
+
this._collapsed = collapsed;
|
|
357
|
+
this._compact = Boolean(compact);
|
|
190
358
|
const next = this.isCollapsed();
|
|
191
|
-
if (previous !== next) {
|
|
359
|
+
if (previous.collapsed !== next) {
|
|
192
360
|
this._renderMode();
|
|
193
361
|
if (outgoingOwnedFocus) {
|
|
194
|
-
|
|
195
|
-
|
|
362
|
+
// The switch's choice when there is a switch; otherwise the strip's own Tab stop, so focus
|
|
363
|
+
// never falls to the page when the choice was made from a menu or a shortcut.
|
|
364
|
+
const control = next ? this._railExpandButton : this.refs.collapse;
|
|
365
|
+
if (control?.isConnected) control.focus();
|
|
366
|
+
else this.focus();
|
|
367
|
+
}
|
|
368
|
+
} else if (this._rail instanceof ModuleBar) {
|
|
369
|
+
// Only the strip's names change; the side menu and focus inside it stay as they are.
|
|
370
|
+
this._rail.setLabels(this._stripLabels());
|
|
371
|
+
this._syncPresentationSwitch();
|
|
372
|
+
}
|
|
373
|
+
const presentation = this.getPresentation();
|
|
374
|
+
if (!silent) {
|
|
375
|
+
if (previous.collapsed !== next) this.emit('collapsechange', { collapsed: next });
|
|
376
|
+
if (previous.presentation !== presentation) {
|
|
377
|
+
this.emit('presentationchange', { presentation, previous: previous.presentation });
|
|
196
378
|
}
|
|
197
|
-
if (!silent) this.emit('collapsechange', { collapsed: next });
|
|
198
379
|
}
|
|
199
380
|
return this;
|
|
200
381
|
}
|
|
@@ -262,7 +443,7 @@ export class AppSidebar extends Component {
|
|
|
262
443
|
return this._rail?.isFlyoutOpen(id) ?? false;
|
|
263
444
|
}
|
|
264
445
|
|
|
265
|
-
/** Focuses the first navigation action. @returns {this} */
|
|
446
|
+
/** Focuses the first navigation action: the module strip's Tab stop in the modules layout. @returns {this} */
|
|
266
447
|
focus() {
|
|
267
448
|
if (this._rail) this._rail.focus();
|
|
268
449
|
else visibleTreeControls(this.refs.list)[0]?.focus();
|
|
@@ -274,6 +455,7 @@ export class AppSidebar extends Component {
|
|
|
274
455
|
const original = this._original;
|
|
275
456
|
this._modeAbort?.abort();
|
|
276
457
|
this._modeAbort = null;
|
|
458
|
+
for (const tooltip of this._stripTooltips.splice(0)) tooltip.destroy();
|
|
277
459
|
this._rail?.destroy();
|
|
278
460
|
this._rail = null;
|
|
279
461
|
super.destroy();
|
|
@@ -288,15 +470,21 @@ export class AppSidebar extends Component {
|
|
|
288
470
|
_renderMode() {
|
|
289
471
|
this._modeAbort?.abort();
|
|
290
472
|
this._modeAbort = new AbortController();
|
|
473
|
+
for (const tooltip of this._stripTooltips.splice(0)) tooltip.destroy();
|
|
474
|
+
this._presentationGroup = null;
|
|
291
475
|
this._rail?.destroy();
|
|
292
476
|
this._rail = null;
|
|
293
477
|
this._railExpandButton = null;
|
|
294
|
-
for (const key of ['expanded', 'list', 'collapse', 'rail'])
|
|
478
|
+
for (const key of ['expanded', 'list', 'collapse', 'rail', 'panelTitle', 'panelLead', 'panelContent', 'panelNav']) {
|
|
479
|
+
delete this.refs[key];
|
|
480
|
+
}
|
|
295
481
|
this.el.replaceChildren();
|
|
296
482
|
this.el.dataset.orientation = this._orientation;
|
|
297
483
|
this.el.dataset.side = this._side;
|
|
298
484
|
this.el.dataset.collapsed = String(this.isCollapsed());
|
|
299
|
-
|
|
485
|
+
this.el.dataset.layout = this._modules ? 'modules' : 'tree';
|
|
486
|
+
if (this._modules) this._renderModules();
|
|
487
|
+
else if (this.isCollapsed()) this._renderRail();
|
|
300
488
|
else this._renderExpanded();
|
|
301
489
|
}
|
|
302
490
|
|
|
@@ -304,12 +492,7 @@ export class AppSidebar extends Component {
|
|
|
304
492
|
_renderExpanded() {
|
|
305
493
|
const headerContent = h('div', { class: 'zx-app-sidebar__header-content' });
|
|
306
494
|
appendSlot(headerContent, this.options.header);
|
|
307
|
-
const collapseButton =
|
|
308
|
-
class: 'zx-app-sidebar__collapse zx-icon-btn',
|
|
309
|
-
type: 'button',
|
|
310
|
-
ariaLabel: this._message('appSidebar.minimize', 'Minimize application sidebar')
|
|
311
|
-
}, icon(this._side === 'right' ? 'chevron-right' : 'chevron-left'));
|
|
312
|
-
collapseButton.hidden = !this.options.collapsible;
|
|
495
|
+
const collapseButton = this._collapseButton();
|
|
313
496
|
const footer = h('footer', { class: 'zx-app-sidebar__footer' });
|
|
314
497
|
appendSlot(footer, this.options.footer);
|
|
315
498
|
footer.hidden = !footer.hasChildNodes();
|
|
@@ -322,10 +505,38 @@ export class AppSidebar extends Component {
|
|
|
322
505
|
this.refs.list = list;
|
|
323
506
|
this.refs.collapse = collapseButton;
|
|
324
507
|
this.el.append(expanded);
|
|
325
|
-
this.
|
|
508
|
+
this._listenTree(list);
|
|
509
|
+
this._renderTree();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** @returns {HTMLButtonElement} The minimize control of an expanded presentation. */
|
|
513
|
+
_collapseButton() {
|
|
514
|
+
const button = /** @type {HTMLButtonElement} */ (h('button', {
|
|
515
|
+
class: 'zx-app-sidebar__collapse zx-icon-btn',
|
|
516
|
+
type: 'button',
|
|
517
|
+
ariaLabel: this._message('appSidebar.minimize', 'Minimize application sidebar')
|
|
518
|
+
}, icon(this._side === 'right' ? 'chevron-right' : 'chevron-left')));
|
|
519
|
+
button.hidden = !this.options.collapsible;
|
|
520
|
+
this._listenMode(button, 'click', () => this.collapse());
|
|
521
|
+
return button;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** @returns {HTMLButtonElement} The expand control at the head of a minimized vertical rail. */
|
|
525
|
+
_expandButton() {
|
|
526
|
+
const button = /** @type {HTMLButtonElement} */ (h('button', {
|
|
527
|
+
class: 'zx-app-sidebar__rail-toggle zx-icon-btn',
|
|
528
|
+
type: 'button',
|
|
529
|
+
ariaLabel: this._message('appSidebar.expand', 'Expand application sidebar')
|
|
530
|
+
}, icon(this._side === 'right' ? 'chevron-left' : 'chevron-right')));
|
|
531
|
+
this._railExpandButton = button;
|
|
532
|
+
this._listenMode(button, 'click', () => this.expand());
|
|
533
|
+
return button;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** @param {HTMLElement} list @returns {void} */
|
|
537
|
+
_listenTree(list) {
|
|
326
538
|
this._listenMode(list, 'click', (event) => this._onTreeClick(event));
|
|
327
539
|
this._listenMode(list, 'keydown', (event) => this._onTreeKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
328
|
-
this._renderTree();
|
|
329
540
|
}
|
|
330
541
|
|
|
331
542
|
/** @returns {void} */
|
|
@@ -335,16 +546,7 @@ export class AppSidebar extends Component {
|
|
|
335
546
|
this.refs.rail = railHost;
|
|
336
547
|
const showToggle = this._orientation === 'vertical' && Boolean(this.options.collapsible);
|
|
337
548
|
const railHeader = h('div', { class: 'zx-app-sidebar__rail-header' });
|
|
338
|
-
if (showToggle)
|
|
339
|
-
const expandButton = h('button', {
|
|
340
|
-
class: 'zx-app-sidebar__rail-toggle zx-icon-btn',
|
|
341
|
-
type: 'button',
|
|
342
|
-
ariaLabel: this._message('appSidebar.expand', 'Expand application sidebar')
|
|
343
|
-
}, icon(this._side === 'right' ? 'chevron-left' : 'chevron-right'));
|
|
344
|
-
railHeader.append(expandButton);
|
|
345
|
-
this._railExpandButton = expandButton;
|
|
346
|
-
this._listenMode(expandButton, 'click', () => this.expand());
|
|
347
|
-
}
|
|
549
|
+
if (showToggle) railHeader.append(this._expandButton());
|
|
348
550
|
appendSlot(railHeader, this.options.railHeader);
|
|
349
551
|
this._rail = new MinimizedPresenter(railHost, {
|
|
350
552
|
items: this._items,
|
|
@@ -368,6 +570,324 @@ export class AppSidebar extends Component {
|
|
|
368
570
|
this._decorateRail();
|
|
369
571
|
}
|
|
370
572
|
|
|
573
|
+
/**
|
|
574
|
+
* The modules layout. Expanded, the strip leaves module menus to the panel beside it; minimized
|
|
575
|
+
* or horizontal, the strip is the whole navigation and menus open as flyouts.
|
|
576
|
+
* @returns {void}
|
|
577
|
+
*/
|
|
578
|
+
_renderModules() {
|
|
579
|
+
const expanded = !this.isCollapsed();
|
|
580
|
+
const stripHeader = h('div', { class: 'zx-app-sidebar__rail-header' });
|
|
581
|
+
appendSlot(stripHeader, this.options.railHeader);
|
|
582
|
+
// One control for both levels, at the foot of the strip above whatever the strip ends with:
|
|
583
|
+
// expanded, the module names and the panel; collapsed, the icons alone.
|
|
584
|
+
const stripFoot = h('div', { class: 'zx-app-sidebar__strip-foot' });
|
|
585
|
+
if (this._orientation === 'vertical' && this.options.collapsible && this.options.presentationSwitch !== false) {
|
|
586
|
+
stripFoot.append(this._presentationSwitch(expanded));
|
|
587
|
+
}
|
|
588
|
+
appendSlot(stripFoot, this.options.railFooter);
|
|
589
|
+
const host = h('nav', { class: 'zx-app-sidebar__strip' });
|
|
590
|
+
const panelId = expanded ? uid('zx-app-sidebar-panel') : null;
|
|
591
|
+
const container = expanded ? h('div', { class: 'zx-app-sidebar__modules' }, host) : host;
|
|
592
|
+
this.el.append(container);
|
|
593
|
+
this.refs.rail = host;
|
|
594
|
+
const presentation = this.options.moduleBar && typeof this.options.moduleBar === 'object'
|
|
595
|
+
? this.options.moduleBar : {};
|
|
596
|
+
/** @type {Record<string, any>} */
|
|
597
|
+
const barOptions = {
|
|
598
|
+
items: this._items,
|
|
599
|
+
active: this._active,
|
|
600
|
+
orientation: this._orientation,
|
|
601
|
+
side: this._side,
|
|
602
|
+
menus: expanded ? 'none' : 'flyout',
|
|
603
|
+
expanded: expanded ? this._shownId : null,
|
|
604
|
+
controls: panelId,
|
|
605
|
+
header: stripHeader.hasChildNodes() ? stripHeader : null,
|
|
606
|
+
footer: stripFoot.hasChildNodes() ? stripFoot : null,
|
|
607
|
+
openDelay: this.options.openDelay,
|
|
608
|
+
closeDelay: this.options.closeDelay,
|
|
609
|
+
renderIcon: this.options.renderIcon,
|
|
610
|
+
onselect: (event) => this._onModuleSelect(event, expanded),
|
|
611
|
+
onflyoutchange: (event) => this.emit('flyoutchange', event.detail)
|
|
612
|
+
};
|
|
613
|
+
for (const key of ['appearance', 'allModules', 'label']) {
|
|
614
|
+
if (presentation[key] != null) barOptions[key] = presentation[key];
|
|
615
|
+
}
|
|
616
|
+
barOptions.labels = this._stripLabels();
|
|
617
|
+
this._rail = new ModuleBar(host, barOptions);
|
|
618
|
+
for (const type of ['zx-select', 'zx-flyoutchange', 'zx-overflowchange', 'zx-allmodules']) {
|
|
619
|
+
this._listenMode(host, type, (event) => event.stopPropagation());
|
|
620
|
+
}
|
|
621
|
+
if (!expanded) return;
|
|
622
|
+
this._listenMode(host, 'keydown', (event) => this._onStripKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
623
|
+
container.append(this._renderPanel(/** @type {string} */ (panelId)));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* The modules layout's switch between its presentations: an APG radio group of one small
|
|
628
|
+
* pictogram per presentation — both menus, the side menu beside module icons, the icons alone.
|
|
629
|
+
* Its checked option is also the collapse control while expanded and the expand control while
|
|
630
|
+
* collapsed, so focus lands on the checked option of its successor across a re-render.
|
|
631
|
+
* @param {boolean} expanded Whether the side menu shows.
|
|
632
|
+
* @returns {HTMLElement}
|
|
633
|
+
*/
|
|
634
|
+
_presentationSwitch(expanded) {
|
|
635
|
+
const labels = this._presentationLabels();
|
|
636
|
+
const group = h('div', {
|
|
637
|
+
class: 'zx-app-sidebar__presentation',
|
|
638
|
+
role: 'radiogroup',
|
|
639
|
+
ariaLabel: this._message('appSidebar.presentation', 'Navigation layout')
|
|
640
|
+
});
|
|
641
|
+
for (const name of this._presentationCycle()) {
|
|
642
|
+
const option = /** @type {HTMLButtonElement} */ (h('button', {
|
|
643
|
+
class: 'zx-app-sidebar__presentation-option',
|
|
644
|
+
type: 'button',
|
|
645
|
+
role: 'radio',
|
|
646
|
+
ariaLabel: labels[name],
|
|
647
|
+
dataset: { presentation: name }
|
|
648
|
+
}, presentationGlyph(name)));
|
|
649
|
+
group.append(option);
|
|
650
|
+
this._stripTooltips.push(new Tooltip(option, {
|
|
651
|
+
content: labels[name],
|
|
652
|
+
placement: this._side === 'right' ? 'left' : 'right',
|
|
653
|
+
delay: 450
|
|
654
|
+
}));
|
|
655
|
+
}
|
|
656
|
+
this._listenMode(group, 'click', (event) => {
|
|
657
|
+
const option = /** @type {Element} */ (event.target).closest?.('.zx-app-sidebar__presentation-option');
|
|
658
|
+
if (option instanceof HTMLElement) this.setPresentation(/** @type {any} */ (option.dataset.presentation));
|
|
659
|
+
});
|
|
660
|
+
this._listenMode(group, 'keydown', (event) => this._onPresentationKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
661
|
+
this._presentationGroup = group;
|
|
662
|
+
this._syncPresentationSwitch();
|
|
663
|
+
const checked = /** @type {HTMLElement|null} */ (group.querySelector('[aria-checked="true"]'));
|
|
664
|
+
if (expanded) this.refs.collapse = checked ?? group;
|
|
665
|
+
else this._railExpandButton = checked ?? group;
|
|
666
|
+
return group;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* The choice between presentations as a menu entry, for an account or settings menu that holds
|
|
671
|
+
* it instead of the strip: `account.setItems([sidebar.presentationChoice(), …])`. It reads the
|
|
672
|
+
* current presentation when called, so rebuild it after `presentationchange`.
|
|
673
|
+
* @param {{label?: string}} [options={}] Row label; omitted resolves `appSidebar.presentation`.
|
|
674
|
+
* @returns {import('../../internal/menu-items.js').MenuChoice}
|
|
675
|
+
*/
|
|
676
|
+
presentationChoice({ label } = {}) {
|
|
677
|
+
const labels = this._presentationLabels();
|
|
678
|
+
return {
|
|
679
|
+
type: 'choice',
|
|
680
|
+
label: label ?? this._message('appSidebar.presentation', 'Navigation layout'),
|
|
681
|
+
value: this.getPresentation(),
|
|
682
|
+
options: this._presentationCycle().map((name) => ({
|
|
683
|
+
value: name, label: labels[name], glyph: () => presentationGlyph(name)
|
|
684
|
+
})),
|
|
685
|
+
onselect: (value) => this.setPresentation(/** @type {any} */ (value))
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** @returns {Record<'expanded'|'compact'|'collapsed', string>} */
|
|
690
|
+
_presentationLabels() {
|
|
691
|
+
return {
|
|
692
|
+
expanded: this._message('appSidebar.presentationExpanded', 'Both menus expanded'),
|
|
693
|
+
compact: this._message('appSidebar.presentationCompact', 'Module menu collapsed'),
|
|
694
|
+
collapsed: this._message('appSidebar.presentationCollapsed', 'Both menus collapsed')
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Arrow keys move through the presentations and choose as they go, as in any radio group. @param {KeyboardEvent} event @returns {void} */
|
|
699
|
+
_onPresentationKeydown(event) {
|
|
700
|
+
const options = /** @type {HTMLElement[]} */ ([...(this._presentationGroup?.querySelectorAll('.zx-app-sidebar__presentation-option') ?? [])]);
|
|
701
|
+
const index = options.indexOf(/** @type {HTMLElement} */ (event.target));
|
|
702
|
+
if (index < 0 || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
703
|
+
let next;
|
|
704
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') next = options[(index + 1) % options.length];
|
|
705
|
+
else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') next = options[(index - 1 + options.length) % options.length];
|
|
706
|
+
else if (event.key === 'Home') next = options[0];
|
|
707
|
+
else if (event.key === 'End') next = options.at(-1);
|
|
708
|
+
else return;
|
|
709
|
+
event.preventDefault();
|
|
710
|
+
const name = next.dataset.presentation;
|
|
711
|
+
next.focus();
|
|
712
|
+
this.setPresentation(/** @type {any} */ (name));
|
|
713
|
+
// A change across collapsed re-renders the strip: focus goes to the new switch's choice.
|
|
714
|
+
const replacement = this._presentationGroup?.querySelector(`.zx-app-sidebar__presentation-option[data-presentation="${name}"]`);
|
|
715
|
+
if (replacement instanceof HTMLElement && replacement !== document.activeElement) replacement.focus();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** Checks the current presentation's option and makes it the group's one Tab stop. @returns {void} */
|
|
719
|
+
_syncPresentationSwitch() {
|
|
720
|
+
const group = this._presentationGroup;
|
|
721
|
+
if (!group) return;
|
|
722
|
+
const current = this.getPresentation();
|
|
723
|
+
for (const option of /** @type {HTMLElement[]} */ ([...group.querySelectorAll('.zx-app-sidebar__presentation-option')])) {
|
|
724
|
+
const checked = option.dataset.presentation === current;
|
|
725
|
+
option.setAttribute('aria-checked', String(checked));
|
|
726
|
+
option.tabIndex = checked ? 0 : -1;
|
|
727
|
+
}
|
|
728
|
+
if (!group.querySelector('[aria-checked="true"]')) {
|
|
729
|
+
const first = /** @type {HTMLElement|null} */ (group.querySelector('.zx-app-sidebar__presentation-option'));
|
|
730
|
+
if (first) first.tabIndex = 0;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** @returns {Array<'expanded'|'compact'|'collapsed'>} The presentations the switch offers, widest first. */
|
|
735
|
+
_presentationCycle() {
|
|
736
|
+
const requested = Array.isArray(this.options.presentations) ? this.options.presentations : PRESENTATIONS;
|
|
737
|
+
// A strip that never names its modules has no 'expanded' apart from 'compact', and one that
|
|
738
|
+
// always names them under the tiles no 'compact' apart from 'expanded': offer only the one.
|
|
739
|
+
const same = this._moduleLabels === 'hidden' ? 'expanded' : this._moduleLabels === 'below' ? 'compact' : null;
|
|
740
|
+
const cycle = [...new Set(requested)].filter((name) => PRESENTATIONS.includes(name)
|
|
741
|
+
&& (name !== 'compact' || this._modules) && name !== same);
|
|
742
|
+
return cycle.length >= 2 ? cycle : ['expanded', 'collapsed'];
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** @returns {'expanded'|'compact'|'collapsed'} */
|
|
746
|
+
_nextPresentation() {
|
|
747
|
+
const cycle = this._presentationCycle();
|
|
748
|
+
const index = cycle.indexOf(this.getPresentation());
|
|
749
|
+
return cycle[(index + 1) % cycle.length];
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* What the strip prints for the current state: `'auto'` names the modules beside their tiles
|
|
754
|
+
* while expanded and leaves the icons alone while collapsed.
|
|
755
|
+
* @returns {'hidden'|'below'|'beside'}
|
|
756
|
+
*/
|
|
757
|
+
_stripLabels() {
|
|
758
|
+
const labels = this._moduleLabels;
|
|
759
|
+
if (labels === 'below') return 'below';
|
|
760
|
+
if (this.isCollapsed() || this._compact) return 'hidden';
|
|
761
|
+
return labels === 'hidden' ? 'hidden' : 'beside';
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** @returns {AppSidebarModule[]} The modules, read through any sections that group them. */
|
|
765
|
+
_moduleList() {
|
|
766
|
+
return /** @type {AppSidebarModule[]} */ (liftSections(this._items));
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** @param {string} panelId @returns {HTMLElement} */
|
|
770
|
+
_renderPanel(panelId) {
|
|
771
|
+
const titleId = uid('zx-app-sidebar-module');
|
|
772
|
+
const title = h('div', { class: 'zx-app-sidebar__panel-title', id: titleId });
|
|
773
|
+
const lead = h('div', { class: 'zx-app-sidebar__panel-lead' });
|
|
774
|
+
const list = h('ul', { class: 'zx-app-sidebar__list' });
|
|
775
|
+
const nav = h('nav', { class: 'zx-app-sidebar__nav', ariaLabelledby: titleId }, list);
|
|
776
|
+
const content = h('div', { class: 'zx-app-sidebar__panel-content' });
|
|
777
|
+
const footer = h('footer', { class: 'zx-app-sidebar__footer' });
|
|
778
|
+
appendSlot(footer, this.options.footer);
|
|
779
|
+
footer.hidden = !footer.hasChildNodes();
|
|
780
|
+
const panel = h('div', { class: 'zx-app-sidebar__panel', id: panelId },
|
|
781
|
+
h('header', { class: 'zx-app-sidebar__header' },
|
|
782
|
+
h('div', { class: 'zx-app-sidebar__header-content' }, title)),
|
|
783
|
+
h('div', { class: 'zx-app-sidebar__panel-body' }, lead, nav, content),
|
|
784
|
+
footer);
|
|
785
|
+
Object.assign(this.refs, {
|
|
786
|
+
expanded: panel, list,
|
|
787
|
+
panelTitle: title, panelLead: lead, panelContent: content, panelNav: nav
|
|
788
|
+
});
|
|
789
|
+
this._listenTree(list);
|
|
790
|
+
this._renderPanelContent();
|
|
791
|
+
return panel;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** Puts the shown module's title, header, menu, and content into the panel. @returns {void} */
|
|
795
|
+
_renderPanelContent() {
|
|
796
|
+
const module = this._shownModule();
|
|
797
|
+
const { panelTitle, panelLead, panelContent, panelNav } = this.refs;
|
|
798
|
+
if (!panelTitle) return;
|
|
799
|
+
panelTitle.textContent = module?.label ?? '';
|
|
800
|
+
// Slot nodes already in place stay put, so a menu refresh never takes focus out of them.
|
|
801
|
+
placeSlots(panelLead, [this.options.header, module?.header]);
|
|
802
|
+
placeSlots(panelContent, [module?.content]);
|
|
803
|
+
/** @type {HTMLElement} */ (panelNav).hidden = !(module?.children?.length);
|
|
804
|
+
this._renderTree();
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Shows a module's panel. Returns false when there is no such panel module or a listener
|
|
809
|
+
* prevented the switch.
|
|
810
|
+
* @param {string|number} id Module id.
|
|
811
|
+
* @param {{silent?: boolean}} [options={}]
|
|
812
|
+
* @returns {boolean}
|
|
813
|
+
*/
|
|
814
|
+
_showModule(id, { silent = false } = {}) {
|
|
815
|
+
if (!this._modules) return false;
|
|
816
|
+
const module = this._moduleList().find((item) => String(item.id) === String(id));
|
|
817
|
+
if (!module || !isPanelModule(module) || module.disabled) return false;
|
|
818
|
+
if (String(module.id) === String(this._shownId)) return true;
|
|
819
|
+
const previous = this._shownId;
|
|
820
|
+
if (!silent && this.emit('modulechange', { item: module, id: module.id, previous }).defaultPrevented) return false;
|
|
821
|
+
this._shownId = module.id;
|
|
822
|
+
if (this._rail instanceof ModuleBar && !this.isCollapsed()) this._rail.setExpanded(module.id);
|
|
823
|
+
if (this.refs.list) {
|
|
824
|
+
this._renderPanelContent();
|
|
825
|
+
// Only the panel that just changed animates; a re-render for any other reason does not.
|
|
826
|
+
const body = this.refs.expanded?.querySelector('.zx-app-sidebar__panel-body');
|
|
827
|
+
if (body instanceof HTMLElement) {
|
|
828
|
+
delete body.dataset.moduleMotion;
|
|
829
|
+
void body.offsetWidth;
|
|
830
|
+
body.dataset.moduleMotion = 'enter';
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** @param {CustomEvent<AppNavSelectDetail>} event @param {boolean} expanded @returns {void} */
|
|
837
|
+
_onModuleSelect(event, expanded) {
|
|
838
|
+
const module = this._moduleList().find((item) => String(item.id) === String(event.detail.id));
|
|
839
|
+
if (module && isPanelModule(module)) {
|
|
840
|
+
// A module with a panel is not a destination: choosing it shows the panel.
|
|
841
|
+
event.preventDefault();
|
|
842
|
+
const shown = this._showModule(module.id);
|
|
843
|
+
if (shown && !expanded && this._orientation === 'vertical') {
|
|
844
|
+
this.expand();
|
|
845
|
+
visibleTreeControls(this.refs.list)[0]?.focus();
|
|
846
|
+
}
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const selected = this.emit('select', event.detail);
|
|
850
|
+
if (selected.defaultPrevented) event.preventDefault();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** The arrow toward the panel shows a module's menu and moves into it. @param {KeyboardEvent} event @returns {void} */
|
|
854
|
+
_onStripKeydown(event) {
|
|
855
|
+
const into = this._side === 'right' ? 'ArrowLeft' : 'ArrowRight';
|
|
856
|
+
if (event.key !== into || event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
857
|
+
const control = /** @type {Element} */ (event.target).closest?.('.zx-module-bar__item[data-app-nav-value-id]');
|
|
858
|
+
if (!control) return;
|
|
859
|
+
const module = this._moduleList().find((item) => String(item.id) === control.getAttribute('data-app-nav-value-id'));
|
|
860
|
+
if (!module || !isPanelModule(module) || !this._showModule(module.id)) return;
|
|
861
|
+
event.preventDefault();
|
|
862
|
+
const target = visibleTreeControls(this.refs.list)[0]
|
|
863
|
+
?? this.refs.expanded?.querySelector('.zx-app-sidebar__panel-body :is(a[href], button, input, select, textarea, [tabindex="0"])');
|
|
864
|
+
if (target instanceof HTMLElement) target.focus();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** @returns {AppSidebarModule|null} */
|
|
868
|
+
_shownModule() {
|
|
869
|
+
return this._moduleList().find((item) => String(item.id) === String(this._shownId)) ?? null;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** @param {string|number|null|undefined} preferred @returns {string|number|null} */
|
|
873
|
+
_resolveShown(preferred) {
|
|
874
|
+
const panel = (id) => {
|
|
875
|
+
if (id == null) return null;
|
|
876
|
+
const module = this._moduleList().find((item) => String(item.id) === String(id));
|
|
877
|
+
return module && isPanelModule(module) ? module : null;
|
|
878
|
+
};
|
|
879
|
+
return panel(preferred)?.id
|
|
880
|
+
?? panel(moduleOf(this._moduleList(), this._active)?.id)?.id
|
|
881
|
+
?? this._moduleList().find(isPanelModule)?.id
|
|
882
|
+
?? null;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** @returns {AppNavItem[]} The items the tree draws: the shown module's menu in the modules layout. */
|
|
886
|
+
_treeItems() {
|
|
887
|
+
if (!this._modules) return this._items;
|
|
888
|
+
return this._shownModule()?.children ?? [];
|
|
889
|
+
}
|
|
890
|
+
|
|
371
891
|
/**
|
|
372
892
|
* Keeps a minimized rail's counts readable and audible.
|
|
373
893
|
*
|
|
@@ -404,7 +924,7 @@ export class AppSidebar extends Component {
|
|
|
404
924
|
? /** @type {Element} */ (document.activeElement).closest('[data-app-nav-id]')
|
|
405
925
|
?.getAttribute('data-app-nav-id')
|
|
406
926
|
: null;
|
|
407
|
-
this.refs.list.replaceChildren(...this.
|
|
927
|
+
this.refs.list.replaceChildren(...this._treeItems().map((item) => this._treeRow(item, 1)));
|
|
408
928
|
this._syncActive();
|
|
409
929
|
if (focused !== null) {
|
|
410
930
|
const replacement = [...this.refs.list.querySelectorAll('[data-app-nav-id]')]
|
|
@@ -415,6 +935,7 @@ export class AppSidebar extends Component {
|
|
|
415
935
|
|
|
416
936
|
/** @param {AppNavItem} item @param {number} level @returns {HTMLElement} */
|
|
417
937
|
_treeRow(item, level) {
|
|
938
|
+
if (item.section) return this._sectionRow(item, level);
|
|
418
939
|
const children = item.children ?? [];
|
|
419
940
|
const hasChildren = children.length > 0;
|
|
420
941
|
const key = String(item.id);
|
|
@@ -442,6 +963,27 @@ export class AppSidebar extends Component {
|
|
|
442
963
|
return row;
|
|
443
964
|
}
|
|
444
965
|
|
|
966
|
+
/**
|
|
967
|
+
* A section: a heading over rows that stay at its own level. It names its list, so a screen
|
|
968
|
+
* reader hears "Records, list, 3 items" rather than a heading floating above unrelated rows.
|
|
969
|
+
* @param {AppNavItem} item Section.
|
|
970
|
+
* @param {number} level Level its rows take.
|
|
971
|
+
* @returns {HTMLElement}
|
|
972
|
+
*/
|
|
973
|
+
_sectionRow(item, level) {
|
|
974
|
+
const labelId = uid('zx-app-sidebar-section');
|
|
975
|
+
const header = h('div', { class: 'zx-app-sidebar__section-header' },
|
|
976
|
+
h('span', { class: 'zx-app-sidebar__section-label', id: labelId }, item.label));
|
|
977
|
+
if (item.actions) {
|
|
978
|
+
const actions = h('span', { class: 'zx-app-sidebar__section-actions' });
|
|
979
|
+
appendSlot(actions, item.actions);
|
|
980
|
+
header.append(actions);
|
|
981
|
+
}
|
|
982
|
+
return h('li', { class: 'zx-app-sidebar__section' }, header,
|
|
983
|
+
h('ul', { class: 'zx-app-sidebar__section-list', ariaLabelledby: labelId },
|
|
984
|
+
(item.children ?? []).map((child) => this._treeRow(child, level))));
|
|
985
|
+
}
|
|
986
|
+
|
|
445
987
|
/** @param {Event} event @returns {void} */
|
|
446
988
|
_onTreeClick(event) {
|
|
447
989
|
const control = /** @type {Element|null} */ (event.target)?.closest?.('[data-app-nav-id]');
|
|
@@ -479,6 +1021,16 @@ export class AppSidebar extends Component {
|
|
|
479
1021
|
if (parentControl instanceof HTMLElement) {
|
|
480
1022
|
event.preventDefault();
|
|
481
1023
|
parentControl.focus();
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
// A top-level row of a module's menu: back out to that module's tile on the strip.
|
|
1027
|
+
if (this._modules && this._side === 'left' && this._rail instanceof ModuleBar) {
|
|
1028
|
+
const tile = this._rail._tiles().find((control) =>
|
|
1029
|
+
control.getAttribute('data-app-nav-value-id') === String(this._shownId));
|
|
1030
|
+
if (tile) {
|
|
1031
|
+
event.preventDefault();
|
|
1032
|
+
tile.focus();
|
|
1033
|
+
}
|
|
482
1034
|
}
|
|
483
1035
|
return;
|
|
484
1036
|
}
|
|
@@ -561,12 +1113,38 @@ export class AppSidebar extends Component {
|
|
|
561
1113
|
_reconcileExpanded() {
|
|
562
1114
|
const branchIds = new Set();
|
|
563
1115
|
walkItems(this._items, (item) => {
|
|
564
|
-
if (item.children?.length) branchIds.add(String(item.id));
|
|
1116
|
+
if (item.children?.length && !item.section) branchIds.add(String(item.id));
|
|
565
1117
|
});
|
|
566
1118
|
for (const id of this._expanded) if (!branchIds.has(id)) this._expanded.delete(id);
|
|
567
1119
|
}
|
|
568
1120
|
}
|
|
569
1121
|
|
|
1122
|
+
/**
|
|
1123
|
+
* The pictogram of a presentation: a small window with its navigation drawn in it — the module
|
|
1124
|
+
* strip, wide when it is named, and the side menu when it shows. Built from boxes in the text
|
|
1125
|
+
* colour, so it follows the theme and forced colours and needs no icon set. Decorative.
|
|
1126
|
+
* @param {'expanded'|'compact'|'collapsed'} presentation
|
|
1127
|
+
* @returns {HTMLElement}
|
|
1128
|
+
*/
|
|
1129
|
+
export function presentationGlyph(presentation) {
|
|
1130
|
+
return /** @type {HTMLElement} */ (h('span', {
|
|
1131
|
+
class: 'zx-app-sidebar__presentation-glyph',
|
|
1132
|
+
ariaHidden: 'true',
|
|
1133
|
+
dataset: { presentation }
|
|
1134
|
+
}, h('span', { class: 'zx-app-sidebar__presentation-strip' }),
|
|
1135
|
+
h('span', { class: 'zx-app-sidebar__presentation-panel' })));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Whether a module shows a panel — a menu, a header, or content of its own — rather than being a
|
|
1140
|
+
* destination itself.
|
|
1141
|
+
* @param {AppSidebarModule} module Top-level item.
|
|
1142
|
+
* @returns {boolean}
|
|
1143
|
+
*/
|
|
1144
|
+
function isPanelModule(module) {
|
|
1145
|
+
return Boolean(module.children?.length || module.header || module.content);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
570
1148
|
/** @param {AppNavItem} item @param {boolean} disclosure @param {boolean} expanded @param {string|null} controls @param {Node|null} visual @returns {HTMLElement} */
|
|
571
1149
|
function navControl(item, disclosure, expanded, controls, visual) {
|
|
572
1150
|
const tag = item.href && !disclosure ? 'a' : 'button';
|
|
@@ -639,6 +1217,32 @@ function appendSlot(target, content) {
|
|
|
639
1217
|
} else if (typeof content === 'object' && typeof content.nodeType === 'number') target.append(content);
|
|
640
1218
|
}
|
|
641
1219
|
|
|
1220
|
+
/**
|
|
1221
|
+
* Fills a slot region with its contents unless exactly those nodes are already there, and hides
|
|
1222
|
+
* the region while it is empty.
|
|
1223
|
+
* @param {HTMLElement} target Slot region.
|
|
1224
|
+
* @param {unknown[]} contents Slot values, as `appendSlot()` takes them.
|
|
1225
|
+
* @returns {void}
|
|
1226
|
+
*/
|
|
1227
|
+
function placeSlots(target, contents) {
|
|
1228
|
+
const staging = document.createDocumentFragment();
|
|
1229
|
+
const nodes = contents.flatMap((content) => {
|
|
1230
|
+
if (content == null) return [];
|
|
1231
|
+
if (typeof content === 'object' && typeof content.toElement === 'function') {
|
|
1232
|
+
const element = content.toElement();
|
|
1233
|
+
return element ? [element] : [];
|
|
1234
|
+
}
|
|
1235
|
+
if (typeof content === 'object' && typeof content.nodeType === 'number') return [content];
|
|
1236
|
+
appendSlot(staging, content);
|
|
1237
|
+
return [...staging.childNodes];
|
|
1238
|
+
});
|
|
1239
|
+
const current = [...target.childNodes];
|
|
1240
|
+
if (nodes.length !== current.length || nodes.some((node, index) => node !== current[index])) {
|
|
1241
|
+
target.replaceChildren(...nodes);
|
|
1242
|
+
}
|
|
1243
|
+
target.hidden = !target.hasChildNodes();
|
|
1244
|
+
}
|
|
1245
|
+
|
|
642
1246
|
/** @param {'vertical'|'horizontal'} orientation @param {unknown} side @returns {'left'|'right'|'top'|'bottom'} */
|
|
643
1247
|
function normalizeSide(orientation, side) {
|
|
644
1248
|
if (orientation === 'horizontal') return side === 'bottom' ? 'bottom' : 'top';
|
|
@@ -661,3 +1265,5 @@ function copyItems(items) {
|
|
|
661
1265
|
|
|
662
1266
|
/** Fired when expanded/minimized presentation changes. @event AppSidebar#collapsechange @type {CustomEvent<{collapsed: boolean}>} */
|
|
663
1267
|
/** Fired when a minimized flyout changes. @event AppSidebar#flyoutchange @type {CustomEvent<{item: AppNavItem, id: string|number, open: boolean}>} */
|
|
1268
|
+
/** Fired when how much of the navigation shows changes. @event AppSidebar#presentationchange @type {CustomEvent<{presentation: 'expanded'|'compact'|'collapsed', previous: 'expanded'|'compact'|'collapsed'}>} */
|
|
1269
|
+
/** Fired before the modules layout's panel switches to another module; preventable. @event AppSidebar#modulechange @type {CustomEvent<{item: AppNavItem, id: string|number, previous: string|number|null}>} */
|