@zeyos/zx 4.4.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 +447 -0
- package/README.md +44 -11
- package/dist/types/components/account-menu/account-menu.d.ts +8 -0
- package/dist/types/components/activity-list/activity-list.d.ts +31 -1
- package/dist/types/components/app-icon/app-icon.d.ts +14 -2
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +293 -17
- package/dist/types/components/avatar/avatar.d.ts +46 -7
- package/dist/types/components/badge/badge.d.ts +21 -1
- package/dist/types/components/button/button.d.ts +15 -0
- 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/chart/chart.d.ts +10 -0
- package/dist/types/components/checklist/checklist.d.ts +9 -0
- package/dist/types/components/code-editor/code-editor.d.ts +6 -17
- 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/datebox/date-range-box.d.ts +5 -1
- package/dist/types/components/datebox/datebox.d.ts +7 -2
- package/dist/types/components/dialog/dialog.d.ts +46 -6
- package/dist/types/components/dropdown/dropdown.d.ts +24 -9
- package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +54 -2
- package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
- package/dist/types/components/fieldset/fieldset.d.ts +26 -1
- package/dist/types/components/file-item/file-item.d.ts +34 -6
- 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/form/form.d.ts +12 -1
- package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
- package/dist/types/components/launcher/launcher.d.ts +268 -15
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +16 -7
- package/dist/types/components/menu-button/menu-button.d.ts +39 -5
- package/dist/types/components/message/message.d.ts +132 -17
- package/dist/types/components/modal/modal.d.ts +43 -2
- 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/color.d.ts +12 -0
- package/dist/types/components/select/glyphs.d.ts +124 -0
- package/dist/types/components/select/select.d.ts +17 -3
- package/dist/types/components/sheet/sheet.d.ts +15 -0
- package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
- package/dist/types/components/tabbox/tabbox.d.ts +9 -0
- package/dist/types/components/table/table.d.ts +311 -18
- package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
- package/dist/types/components/tree/tree.d.ts +13 -0
- 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/breakpoint.d.ts +1 -1
- package/dist/types/core/component.d.ts +12 -2
- package/dist/types/core/i18n.d.ts +27 -3
- package/dist/types/core/icons.d.ts +23 -25
- package/dist/types/core/keyboard.d.ts +98 -1
- package/dist/types/core/messages.d.ts +382 -0
- package/dist/types/core/motion.d.ts +59 -0
- package/dist/types/core/overlay-host.d.ts +42 -1
- package/dist/types/core/overlay-stack.d.ts +260 -0
- package/dist/types/index.d.ts +10 -5
- package/dist/types/internal/app-rail.d.ts +38 -0
- package/dist/types/internal/menu-items.d.ts +71 -3
- package/dist/types/zeyos/connect.d.ts +6 -0
- package/dist/types/zeyos/form.d.ts +50 -9
- package/dist/types/zeyos/index.d.ts +2 -1
- 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 +110 -5
- package/dist/types/zeyos/schema.d.ts +24 -1
- package/dist/types/zeyos/table.d.ts +345 -22
- package/dist/types/zeyos/view.d.ts +26 -2
- package/dist/zx-zeyos.esm.js +6178 -1008
- package/dist/zx-zeyos.esm.min.js +53 -5
- package/dist/zx.css +2861 -827
- package/dist/zx.esm.js +8909 -1480
- package/dist/zx.esm.min.js +55 -7
- package/dist/zx.global.js +8911 -1482
- package/dist/zx.global.min.js +55 -7
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1079 -117
- package/docs/llms.md +956 -154
- package/docs/llms.txt +9 -5
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.css +35 -10
- package/src/components/account-menu/account-menu.js +12 -0
- package/src/components/activity-item/activity-item.css +1 -1
- package/src/components/activity-item/activity-item.js +3 -0
- package/src/components/activity-list/activity-list.css +38 -3
- package/src/components/activity-list/activity-list.js +61 -1
- package/src/components/app-icon/app-icon.css +14 -3
- package/src/components/app-icon/app-icon.js +11 -1
- package/src/components/app-rail/app-rail.css +59 -79
- package/src/components/app-sidebar/app-sidebar.css +304 -168
- package/src/components/app-sidebar/app-sidebar.js +667 -46
- package/src/components/aurora/aurora.css +5 -3
- package/src/components/avatar/avatar.css +32 -7
- package/src/components/avatar/avatar.js +72 -8
- package/src/components/badge/badge.css +54 -2
- package/src/components/badge/badge.js +29 -2
- package/src/components/breadcrumb/breadcrumb.css +1 -1
- package/src/components/button/button.css +67 -9
- package/src/components/button/button.js +38 -0
- package/src/components/calendar/calendar.css +47 -47
- package/src/components/calendar/calendar.js +26 -52
- package/src/components/card/card.css +4 -4
- package/src/components/card-view/card-view.css +14 -12
- package/src/components/card-view/card-view.js +13 -9
- package/src/components/card-view/record-card.js +14 -3
- package/src/components/chart/chart.css +4 -1
- package/src/components/chart/chart.js +12 -0
- package/src/components/check-button/check-button.css +10 -2
- package/src/components/checklist/checklist.css +9 -2
- package/src/components/checklist/checklist.js +11 -5
- package/src/components/checklist/field-adapter.js +3 -1
- package/src/components/code-editor/code-editor.css +7 -6
- package/src/components/code-editor/code-editor.js +19 -1
- package/src/components/code-editor/field-adapter.js +9 -2
- package/src/components/context-menu/context-menu.css +9 -4
- package/src/components/context-menu/context-menu.js +8 -2
- package/src/components/copy/copy-button.css +6 -0
- package/src/components/copy/copy-input.css +9 -4
- package/src/components/data-filter/data-filter.css +5 -4
- package/src/components/data-filter/data-filter.js +2 -2
- package/src/components/date-picker/date-picker.css +6 -6
- package/src/components/date-picker/date-range-picker.css +3 -3
- package/src/components/date-picker/time-picker.css +3 -3
- package/src/components/datebox/date-range-box.css +79 -2
- package/src/components/datebox/date-range-box.js +13 -2
- package/src/components/datebox/datebox.css +79 -2
- package/src/components/datebox/datebox.js +13 -2
- package/src/components/dialog/dialog.css +26 -0
- package/src/components/dialog/dialog.js +99 -17
- package/src/components/dock/dock.css +5 -4
- package/src/components/dropdown/dropdown.css +1 -1
- package/src/components/dropdown/dropdown.js +159 -72
- package/src/components/empty-state/empty-state.css +2 -2
- package/src/components/entity-ref/entity-ref.css +1 -1
- package/src/components/entity-ref/entity-ref.js +1 -1
- package/src/components/field/field.css +28 -9
- package/src/components/field/field.js +101 -29
- package/src/components/field-upload/field-upload.css +1 -1
- package/src/components/fieldset/external-adapter.js +363 -0
- package/src/components/fieldset/fieldset.css +5 -3
- package/src/components/fieldset/fieldset.js +38 -64
- package/src/components/file-item/file-item.js +144 -10
- package/src/components/file-list/file-list.js +144 -21
- package/src/components/filter/filter.css +86 -2
- package/src/components/filter/filter.js +307 -11
- package/src/components/filter-panel/filter-panel.css +1 -1
- package/src/components/filter-panel/filter-panel.js +149 -32
- package/src/components/finder/finder.css +16 -14
- package/src/components/form/form.css +5 -4
- package/src/components/form/form.js +11 -2
- package/src/components/grid/grid.css +1 -1
- package/src/components/groupbox/groupbox.css +1 -1
- package/src/components/kanban-view/kanban-card.js +11 -3
- package/src/components/kanban-view/kanban-view.css +42 -18
- package/src/components/kanban-view/kanban-view.js +9 -6
- package/src/components/launcher/launcher.css +108 -26
- package/src/components/launcher/launcher.js +530 -42
- package/src/components/list-toolbar/list-toolbar.css +13 -8
- package/src/components/list-toolbar/list-toolbar.js +40 -14
- package/src/components/master-panel/master-panel.css +2 -0
- package/src/components/master-panel/master-panel.js +0 -1
- package/src/components/menu-button/menu-button.css +72 -4
- package/src/components/menu-button/menu-button.js +160 -18
- package/src/components/message/message.css +33 -3
- package/src/components/message/message.js +264 -22
- package/src/components/modal/modal.css +1 -1
- package/src/components/modal/modal.js +83 -15
- 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/navigation-bar/navigation-bar.css +18 -9
- package/src/components/number-field/field-adapter.js +3 -1
- package/src/components/number-field/number-field.css +70 -0
- package/src/components/number-field/number-field.js +2 -0
- package/src/components/pagination/pagination.css +13 -11
- package/src/components/pagination/pagination.js +20 -4
- package/src/components/panel/panel.css +4 -3
- package/src/components/questionnaire/questionnaire.css +4 -4
- package/src/components/rating/field-adapter.js +2 -1
- package/src/components/rating/rating.css +8 -1
- package/src/components/search/search.css +4 -1
- package/src/components/select/color.js +22 -0
- package/src/components/select/field-adapter.js +2 -0
- package/src/components/select/glyphs.js +165 -0
- package/src/components/select/select.css +138 -45
- package/src/components/select/select.js +92 -47
- package/src/components/sheet/sheet.css +12 -9
- package/src/components/sheet/sheet.js +74 -32
- package/src/components/slider/field-adapter.js +6 -1
- package/src/components/slider/slider.css +18 -4
- package/src/components/slider/slider.js +1 -1
- package/src/components/sort-control/sort-control.css +4 -1
- package/src/components/split-view/split-view.css +1 -1
- package/src/components/stat-tile/stat-tile.css +42 -47
- package/src/components/stat-tile/stat-tile.js +17 -6
- package/src/components/stepper/stepper.css +1 -1
- package/src/components/tabbox/tabbox.css +41 -32
- package/src/components/tabbox/tabbox.js +5 -1
- package/src/components/table/table.css +187 -37
- package/src/components/table/table.js +718 -73
- package/src/components/tag-picker/field-adapter.js +2 -1
- package/src/components/tag-picker/tag-picker.css +83 -5
- package/src/components/tag-picker/tag-picker.js +25 -8
- package/src/components/timebox/timebox.css +22 -2
- package/src/components/toggle/toggle.css +12 -1
- package/src/components/toolbar/toolbar.js +2 -2
- package/src/components/tooltip/tooltip.css +4 -0
- package/src/components/tooltip/tooltip.js +44 -19
- package/src/components/tree/tree.css +26 -17
- package/src/components/tree/tree.js +31 -0
- package/src/components/value-list/value-list.css +3 -3
- 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/breakpoint.js +1 -1
- 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 +204 -61
- package/src/core/keyboard.js +206 -11
- package/src/core/messages.js +427 -0
- package/src/core/motion.js +147 -0
- package/src/core/overlay-host.js +77 -3
- package/src/core/overlay-stack.js +487 -0
- package/src/core/position.js +22 -3
- package/src/index.js +10 -5
- package/src/internal/app-rail.js +135 -55
- package/src/internal/menu-items.js +61 -3
- package/src/zeyos/connect.js +6 -2
- package/src/zeyos/form.js +69 -27
- package/src/zeyos/index.js +4 -1
- package/src/zeyos/launcher.js +8 -3
- package/src/zeyos/module-bar.js +181 -0
- package/src/zeyos/query.js +197 -13
- package/src/zeyos/schema.js +144 -27
- package/src/zeyos/table.js +287 -84
- package/src/zeyos/view.js +49 -93
- package/styles/base.css +72 -1
- package/styles/icons.css +11 -1
- package/styles/print.css +1 -1
- package/styles/tokens/dark.css +117 -37
- package/styles/tokens/global.css +121 -27
- package/styles/tokens/semantic.css +140 -43
- package/styles/tokens/themes.css +7 -6
- package/styles/zx.css +3 -1
- package/styles/tokens/density.css +0 -9
|
@@ -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,
|
|
@@ -78,7 +127,9 @@ export class AppSidebar extends Component {
|
|
|
78
127
|
/** @returns {HTMLElement} */
|
|
79
128
|
render() {
|
|
80
129
|
this._createdRoot = this.el === null;
|
|
81
|
-
|
|
130
|
+
// A plain box: the <nav> inside is the landmark. An <aside> here would add an unnamed
|
|
131
|
+
// complementary landmark around every navigation column.
|
|
132
|
+
const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
|
|
82
133
|
this.el = root;
|
|
83
134
|
this._original = this._createdRoot ? null : {
|
|
84
135
|
attributes: Array.from(root.attributes, (attribute) => [attribute.name, attribute.value]),
|
|
@@ -91,6 +142,16 @@ export class AppSidebar extends Component {
|
|
|
91
142
|
this._collapsed = Boolean(this.options.collapsed);
|
|
92
143
|
this._orientation = this.options.orientation === 'horizontal' ? 'horizontal' : 'vertical';
|
|
93
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;
|
|
94
155
|
this._modeAbort = null;
|
|
95
156
|
root.replaceChildren();
|
|
96
157
|
return root;
|
|
@@ -100,34 +161,131 @@ export class AppSidebar extends Component {
|
|
|
100
161
|
setItems(items) {
|
|
101
162
|
this._items = normalizeAppItems(items);
|
|
102
163
|
this._reconcileExpanded();
|
|
103
|
-
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) {
|
|
104
170
|
this._rail.setItems(this._items);
|
|
105
171
|
this._decorateRail();
|
|
106
172
|
} else this._renderTree();
|
|
107
173
|
return this;
|
|
108
174
|
}
|
|
109
175
|
|
|
110
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* Returns a copy of the normalized items. A leaf has no `children` property, as it was written,
|
|
178
|
+
* rather than the empty list normalization gives it.
|
|
179
|
+
* @returns {AppNavItem[]}
|
|
180
|
+
*/
|
|
111
181
|
getItems() {
|
|
112
|
-
return this._items
|
|
113
|
-
...item,
|
|
114
|
-
children: item.children?.map((child) => ({ ...child }))
|
|
115
|
-
}));
|
|
182
|
+
return copyItems(this._items);
|
|
116
183
|
}
|
|
117
184
|
|
|
118
|
-
/**
|
|
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
|
+
*/
|
|
119
191
|
setActive(id) {
|
|
120
192
|
this._active = id;
|
|
121
193
|
if (this._rail) this._rail.setActive(id);
|
|
122
|
-
|
|
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();
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
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();
|
|
123
224
|
return this;
|
|
124
225
|
}
|
|
125
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
|
+
|
|
126
284
|
/** Replaces expanded branch ids. @param {Array<string|number>} ids @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
127
285
|
setExpanded(ids, { silent = false } = {}) {
|
|
128
286
|
this._expanded = new Set(Array.isArray(ids) ? ids.map(String) : []);
|
|
129
287
|
this._reconcileExpanded();
|
|
130
|
-
if (
|
|
288
|
+
if (this.refs.list) this._renderTree();
|
|
131
289
|
if (!silent) this.emit('branchchange', {
|
|
132
290
|
item: null,
|
|
133
291
|
id: null,
|
|
@@ -149,12 +307,12 @@ export class AppSidebar extends Component {
|
|
|
149
307
|
/** Toggles one inline branch. @param {string|number} id @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
150
308
|
toggleBranch(id, { silent = false } = {}) {
|
|
151
309
|
const item = findItem(this._items, id);
|
|
152
|
-
if (!item?.children?.length) return this;
|
|
310
|
+
if (!item?.children?.length || item.section) return this;
|
|
153
311
|
const key = String(id);
|
|
154
312
|
const expanded = !this._expanded.has(key);
|
|
155
313
|
if (expanded) this._expanded.add(key);
|
|
156
314
|
else this._expanded.delete(key);
|
|
157
|
-
if (
|
|
315
|
+
if (this.refs.list) {
|
|
158
316
|
this._renderTree();
|
|
159
317
|
if (expanded) this._markBranchMotion(key);
|
|
160
318
|
}
|
|
@@ -179,19 +337,45 @@ export class AppSidebar extends Component {
|
|
|
179
337
|
|
|
180
338
|
/** Sets the saved vertical expanded/minimized state. @param {boolean} collapsed @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
181
339
|
setCollapsed(collapsed, { silent = false } = {}) {
|
|
182
|
-
|
|
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() };
|
|
183
353
|
const active = document.activeElement;
|
|
184
354
|
const outgoingOwnedFocus = Boolean(active
|
|
185
355
|
&& (this.el.contains(active) || this._rail?._containsNode(active)));
|
|
186
|
-
this._collapsed =
|
|
356
|
+
this._collapsed = collapsed;
|
|
357
|
+
this._compact = Boolean(compact);
|
|
187
358
|
const next = this.isCollapsed();
|
|
188
|
-
if (previous !== next) {
|
|
359
|
+
if (previous.collapsed !== next) {
|
|
189
360
|
this._renderMode();
|
|
190
361
|
if (outgoingOwnedFocus) {
|
|
191
|
-
|
|
192
|
-
|
|
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 });
|
|
193
378
|
}
|
|
194
|
-
if (!silent) this.emit('collapsechange', { collapsed: next });
|
|
195
379
|
}
|
|
196
380
|
return this;
|
|
197
381
|
}
|
|
@@ -259,7 +443,7 @@ export class AppSidebar extends Component {
|
|
|
259
443
|
return this._rail?.isFlyoutOpen(id) ?? false;
|
|
260
444
|
}
|
|
261
445
|
|
|
262
|
-
/** 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} */
|
|
263
447
|
focus() {
|
|
264
448
|
if (this._rail) this._rail.focus();
|
|
265
449
|
else visibleTreeControls(this.refs.list)[0]?.focus();
|
|
@@ -271,6 +455,7 @@ export class AppSidebar extends Component {
|
|
|
271
455
|
const original = this._original;
|
|
272
456
|
this._modeAbort?.abort();
|
|
273
457
|
this._modeAbort = null;
|
|
458
|
+
for (const tooltip of this._stripTooltips.splice(0)) tooltip.destroy();
|
|
274
459
|
this._rail?.destroy();
|
|
275
460
|
this._rail = null;
|
|
276
461
|
super.destroy();
|
|
@@ -285,15 +470,21 @@ export class AppSidebar extends Component {
|
|
|
285
470
|
_renderMode() {
|
|
286
471
|
this._modeAbort?.abort();
|
|
287
472
|
this._modeAbort = new AbortController();
|
|
473
|
+
for (const tooltip of this._stripTooltips.splice(0)) tooltip.destroy();
|
|
474
|
+
this._presentationGroup = null;
|
|
288
475
|
this._rail?.destroy();
|
|
289
476
|
this._rail = null;
|
|
290
477
|
this._railExpandButton = null;
|
|
291
|
-
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
|
+
}
|
|
292
481
|
this.el.replaceChildren();
|
|
293
482
|
this.el.dataset.orientation = this._orientation;
|
|
294
483
|
this.el.dataset.side = this._side;
|
|
295
484
|
this.el.dataset.collapsed = String(this.isCollapsed());
|
|
296
|
-
|
|
485
|
+
this.el.dataset.layout = this._modules ? 'modules' : 'tree';
|
|
486
|
+
if (this._modules) this._renderModules();
|
|
487
|
+
else if (this.isCollapsed()) this._renderRail();
|
|
297
488
|
else this._renderExpanded();
|
|
298
489
|
}
|
|
299
490
|
|
|
@@ -301,12 +492,7 @@ export class AppSidebar extends Component {
|
|
|
301
492
|
_renderExpanded() {
|
|
302
493
|
const headerContent = h('div', { class: 'zx-app-sidebar__header-content' });
|
|
303
494
|
appendSlot(headerContent, this.options.header);
|
|
304
|
-
const collapseButton =
|
|
305
|
-
class: 'zx-app-sidebar__collapse zx-icon-btn',
|
|
306
|
-
type: 'button',
|
|
307
|
-
ariaLabel: this._message('appSidebar.minimize', 'Minimize application sidebar')
|
|
308
|
-
}, icon(this._side === 'right' ? 'chevron-right' : 'chevron-left'));
|
|
309
|
-
collapseButton.hidden = !this.options.collapsible;
|
|
495
|
+
const collapseButton = this._collapseButton();
|
|
310
496
|
const footer = h('footer', { class: 'zx-app-sidebar__footer' });
|
|
311
497
|
appendSlot(footer, this.options.footer);
|
|
312
498
|
footer.hidden = !footer.hasChildNodes();
|
|
@@ -319,10 +505,38 @@ export class AppSidebar extends Component {
|
|
|
319
505
|
this.refs.list = list;
|
|
320
506
|
this.refs.collapse = collapseButton;
|
|
321
507
|
this.el.append(expanded);
|
|
322
|
-
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) {
|
|
323
538
|
this._listenMode(list, 'click', (event) => this._onTreeClick(event));
|
|
324
539
|
this._listenMode(list, 'keydown', (event) => this._onTreeKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
325
|
-
this._renderTree();
|
|
326
540
|
}
|
|
327
541
|
|
|
328
542
|
/** @returns {void} */
|
|
@@ -332,16 +546,7 @@ export class AppSidebar extends Component {
|
|
|
332
546
|
this.refs.rail = railHost;
|
|
333
547
|
const showToggle = this._orientation === 'vertical' && Boolean(this.options.collapsible);
|
|
334
548
|
const railHeader = h('div', { class: 'zx-app-sidebar__rail-header' });
|
|
335
|
-
if (showToggle)
|
|
336
|
-
const expandButton = h('button', {
|
|
337
|
-
class: 'zx-app-sidebar__rail-toggle zx-icon-btn',
|
|
338
|
-
type: 'button',
|
|
339
|
-
ariaLabel: this._message('appSidebar.expand', 'Expand application sidebar')
|
|
340
|
-
}, icon(this._side === 'right' ? 'chevron-left' : 'chevron-right'));
|
|
341
|
-
railHeader.append(expandButton);
|
|
342
|
-
this._railExpandButton = expandButton;
|
|
343
|
-
this._listenMode(expandButton, 'click', () => this.expand());
|
|
344
|
-
}
|
|
549
|
+
if (showToggle) railHeader.append(this._expandButton());
|
|
345
550
|
appendSlot(railHeader, this.options.railHeader);
|
|
346
551
|
this._rail = new MinimizedPresenter(railHost, {
|
|
347
552
|
items: this._items,
|
|
@@ -365,6 +570,324 @@ export class AppSidebar extends Component {
|
|
|
365
570
|
this._decorateRail();
|
|
366
571
|
}
|
|
367
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
|
+
|
|
368
891
|
/**
|
|
369
892
|
* Keeps a minimized rail's counts readable and audible.
|
|
370
893
|
*
|
|
@@ -401,7 +924,7 @@ export class AppSidebar extends Component {
|
|
|
401
924
|
? /** @type {Element} */ (document.activeElement).closest('[data-app-nav-id]')
|
|
402
925
|
?.getAttribute('data-app-nav-id')
|
|
403
926
|
: null;
|
|
404
|
-
this.refs.list.replaceChildren(...this.
|
|
927
|
+
this.refs.list.replaceChildren(...this._treeItems().map((item) => this._treeRow(item, 1)));
|
|
405
928
|
this._syncActive();
|
|
406
929
|
if (focused !== null) {
|
|
407
930
|
const replacement = [...this.refs.list.querySelectorAll('[data-app-nav-id]')]
|
|
@@ -412,6 +935,7 @@ export class AppSidebar extends Component {
|
|
|
412
935
|
|
|
413
936
|
/** @param {AppNavItem} item @param {number} level @returns {HTMLElement} */
|
|
414
937
|
_treeRow(item, level) {
|
|
938
|
+
if (item.section) return this._sectionRow(item, level);
|
|
415
939
|
const children = item.children ?? [];
|
|
416
940
|
const hasChildren = children.length > 0;
|
|
417
941
|
const key = String(item.id);
|
|
@@ -439,6 +963,27 @@ export class AppSidebar extends Component {
|
|
|
439
963
|
return row;
|
|
440
964
|
}
|
|
441
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
|
+
|
|
442
987
|
/** @param {Event} event @returns {void} */
|
|
443
988
|
_onTreeClick(event) {
|
|
444
989
|
const control = /** @type {Element|null} */ (event.target)?.closest?.('[data-app-nav-id]');
|
|
@@ -476,6 +1021,16 @@ export class AppSidebar extends Component {
|
|
|
476
1021
|
if (parentControl instanceof HTMLElement) {
|
|
477
1022
|
event.preventDefault();
|
|
478
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
|
+
}
|
|
479
1034
|
}
|
|
480
1035
|
return;
|
|
481
1036
|
}
|
|
@@ -558,12 +1113,38 @@ export class AppSidebar extends Component {
|
|
|
558
1113
|
_reconcileExpanded() {
|
|
559
1114
|
const branchIds = new Set();
|
|
560
1115
|
walkItems(this._items, (item) => {
|
|
561
|
-
if (item.children?.length) branchIds.add(String(item.id));
|
|
1116
|
+
if (item.children?.length && !item.section) branchIds.add(String(item.id));
|
|
562
1117
|
});
|
|
563
1118
|
for (const id of this._expanded) if (!branchIds.has(id)) this._expanded.delete(id);
|
|
564
1119
|
}
|
|
565
1120
|
}
|
|
566
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
|
+
|
|
567
1148
|
/** @param {AppNavItem} item @param {boolean} disclosure @param {boolean} expanded @param {string|null} controls @param {Node|null} visual @returns {HTMLElement} */
|
|
568
1149
|
function navControl(item, disclosure, expanded, controls, visual) {
|
|
569
1150
|
const tag = item.href && !disclosure ? 'a' : 'button';
|
|
@@ -636,6 +1217,32 @@ function appendSlot(target, content) {
|
|
|
636
1217
|
} else if (typeof content === 'object' && typeof content.nodeType === 'number') target.append(content);
|
|
637
1218
|
}
|
|
638
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
|
+
|
|
639
1246
|
/** @param {'vertical'|'horizontal'} orientation @param {unknown} side @returns {'left'|'right'|'top'|'bottom'} */
|
|
640
1247
|
function normalizeSide(orientation, side) {
|
|
641
1248
|
if (orientation === 'horizontal') return side === 'bottom' ? 'bottom' : 'top';
|
|
@@ -644,5 +1251,19 @@ function normalizeSide(orientation, side) {
|
|
|
644
1251
|
|
|
645
1252
|
/** Fired when a destination/action is activated. @event AppSidebar#select @type {CustomEvent<AppNavSelectDetail>} */
|
|
646
1253
|
/** Fired when an inline branch changes. @event AppSidebar#branchchange @type {CustomEvent<{item: AppNavItem|null, id: string|number|null, expanded: boolean|null, ids: Array<string|number>}>} */
|
|
1254
|
+
/**
|
|
1255
|
+
* Copies a navigation tree, leaving out the empty `children` lists normalization adds to leaves.
|
|
1256
|
+
* @param {AppNavItem[]} items Normalized items.
|
|
1257
|
+
* @returns {AppNavItem[]}
|
|
1258
|
+
*/
|
|
1259
|
+
function copyItems(items) {
|
|
1260
|
+
return items.map((item) => {
|
|
1261
|
+
const { children, ...rest } = item;
|
|
1262
|
+
return children?.length ? { ...rest, children: copyItems(children) } : rest;
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
647
1266
|
/** Fired when expanded/minimized presentation changes. @event AppSidebar#collapsechange @type {CustomEvent<{collapsed: boolean}>} */
|
|
648
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}>} */
|