@zeyos/zx 2.1.0 → 2.2.1
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 +51 -0
- package/README.md +15 -0
- package/dist/types/compat/base.d.ts +156 -0
- package/dist/types/compat/globals.d.ts +20 -0
- package/dist/types/compat/index.d.ts +128 -0
- package/dist/types/compat/map/basic.d.ts +175 -0
- package/dist/types/compat/map/data.d.ts +102 -0
- package/dist/types/compat/map/date-time.d.ts +80 -0
- package/dist/types/compat/map/factory.d.ts +9 -0
- package/dist/types/compat/map/form.d.ts +98 -0
- package/dist/types/compat/map/helpers.d.ts +12 -0
- package/dist/types/compat/map/http.d.ts +36 -0
- package/dist/types/compat/map/layout.d.ts +106 -0
- package/dist/types/compat/map/options.d.ts +80 -0
- package/dist/types/compat/map/select.d.ts +95 -0
- package/dist/types/compat/map/stubs.d.ts +32 -0
- package/dist/types/compat/map/utilities.d.ts +37 -0
- package/dist/types/compat-entry.d.ts +5 -0
- package/dist/types/components/badge/badge.d.ts +46 -0
- package/dist/types/components/breadcrumb/breadcrumb.d.ts +200 -0
- package/dist/types/components/button/button.d.ts +42 -0
- package/dist/types/components/check-button/check-button.d.ts +98 -0
- package/dist/types/components/checklist/checklist.d.ts +143 -0
- package/dist/types/components/checklist/field-adapter.d.ts +5 -0
- package/dist/types/components/context-menu/context-menu.d.ts +130 -0
- package/dist/types/components/copy/copy-button.d.ts +48 -0
- package/dist/types/components/copy/copy-input.d.ts +91 -0
- package/dist/types/components/data-filter/data-filter.d.ts +250 -0
- package/dist/types/components/data-filter/filter-core.d.ts +52 -0
- package/dist/types/components/date-picker/date-picker.d.ts +137 -0
- package/dist/types/components/date-picker/date-range-picker.d.ts +350 -0
- package/dist/types/components/date-picker/field-adapter.d.ts +6 -0
- package/dist/types/components/date-picker/month-picker.d.ts +75 -0
- package/dist/types/components/date-picker/range-field-adapter.d.ts +7 -0
- package/dist/types/components/date-picker/time-picker.d.ts +127 -0
- package/dist/types/components/datebox/date-range-box.d.ts +218 -0
- package/dist/types/components/datebox/datebox.d.ts +160 -0
- package/dist/types/components/datebox/field-adapter.d.ts +5 -0
- package/dist/types/components/dialog/dialog.d.ts +284 -0
- package/dist/types/components/dropdown/dropdown.d.ts +118 -0
- package/dist/types/components/empty-state/empty-state.d.ts +38 -0
- package/dist/types/components/field/field.d.ts +231 -0
- package/dist/types/components/field-upload/field-adapter.d.ts +5 -0
- package/dist/types/components/field-upload/field-upload.d.ts +213 -0
- package/dist/types/components/fieldset/fieldset.d.ts +125 -0
- package/dist/types/components/finder/finder.d.ts +252 -0
- package/dist/types/components/form/form.d.ts +194 -0
- package/dist/types/components/groupbox/groupbox.d.ts +89 -0
- package/dist/types/components/layout/layout.d.ts +132 -0
- package/dist/types/components/loading/inline-loading.d.ts +86 -0
- package/dist/types/components/loading/progress-bar.d.ts +168 -0
- package/dist/types/components/loading/spinner.d.ts +34 -0
- package/dist/types/components/master-panel/master-panel.d.ts +112 -0
- package/dist/types/components/menu-button/menu-button.d.ts +142 -0
- package/dist/types/components/message/message.d.ts +212 -0
- package/dist/types/components/modal/modal.d.ts +105 -0
- package/dist/types/components/multi-value-editor/field-adapter.d.ts +5 -0
- package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +66 -0
- package/dist/types/components/navigation-bar/navigation-bar.d.ts +229 -0
- package/dist/types/components/number-field/field-adapter.d.ts +5 -0
- package/dist/types/components/number-field/number-field.d.ts +292 -0
- package/dist/types/components/pagination/pagination.d.ts +267 -0
- package/dist/types/components/panel/panel.d.ts +194 -0
- package/dist/types/components/permission/permission.d.ts +93 -0
- package/dist/types/components/rating/field-adapter.d.ts +5 -0
- package/dist/types/components/rating/rating.d.ts +213 -0
- package/dist/types/components/search/search.d.ts +98 -0
- package/dist/types/components/select/field-adapter.d.ts +5 -0
- package/dist/types/components/select/filter.d.ts +11 -0
- package/dist/types/components/select/select.d.ts +235 -0
- package/dist/types/components/skeleton/skeleton.d.ts +97 -0
- package/dist/types/components/slider/field-adapter.d.ts +5 -0
- package/dist/types/components/slider/slider.d.ts +216 -0
- package/dist/types/components/split-view/split-view.d.ts +335 -0
- package/dist/types/components/stepper/stepper.d.ts +314 -0
- package/dist/types/components/stepper/steps.d.ts +131 -0
- package/dist/types/components/tabbox/tabbox.d.ts +230 -0
- package/dist/types/components/table/sort.d.ts +49 -0
- package/dist/types/components/table/table.d.ts +872 -0
- package/dist/types/components/tag-picker/field-adapter.d.ts +5 -0
- package/dist/types/components/tag-picker/tag-picker.d.ts +375 -0
- package/dist/types/components/timebox/field-adapter.d.ts +5 -0
- package/dist/types/components/timebox/timebox.d.ts +121 -0
- package/dist/types/components/toggle/field-adapter.d.ts +5 -0
- package/dist/types/components/toggle/toggle.d.ts +94 -0
- package/dist/types/components/toolbar/toolbar.d.ts +230 -0
- package/dist/types/components/tooltip/tooltip.d.ts +183 -0
- package/dist/types/components/tree/hierarchy.d.ts +113 -0
- package/dist/types/components/tree/tree.d.ts +383 -0
- package/dist/types/components/truncate/truncate.d.ts +61 -0
- package/dist/types/components/value-list/field-adapter.d.ts +5 -0
- package/dist/types/components/value-list/value-list.d.ts +143 -0
- package/dist/types/core/breakpoint.d.ts +68 -0
- package/dist/types/core/component.d.ts +115 -0
- package/dist/types/core/date.d.ts +50 -0
- package/dist/types/core/dom.d.ts +86 -0
- package/dist/types/core/export.d.ts +70 -0
- package/dist/types/core/fontawesome.d.ts +107 -0
- package/dist/types/core/format.d.ts +107 -0
- package/dist/types/core/http.d.ts +122 -0
- package/dist/types/core/i18n.d.ts +35 -0
- package/dist/types/core/icons.d.ts +137 -0
- package/dist/types/core/keyboard.d.ts +72 -0
- package/dist/types/core/position.d.ts +45 -0
- package/dist/types/core/storage.d.ts +46 -0
- package/dist/types/core/util.d.ts +117 -0
- package/dist/types/elements/define.d.ts +73 -0
- package/dist/types/elements/reflect.d.ts +27 -0
- package/dist/types/index.d.ts +79 -0
- package/dist/types/zeyos/connect.d.ts +65 -0
- package/dist/types/zeyos/form.d.ts +140 -0
- package/dist/types/zeyos/icons.d.ts +104 -0
- package/dist/types/zeyos/index.d.ts +12 -0
- package/dist/types/zeyos/modules.d.ts +110 -0
- package/dist/types/zeyos/query.d.ts +157 -0
- package/dist/types/zeyos/schema.d.ts +79 -0
- package/dist/types/zeyos/select.d.ts +167 -0
- package/dist/types/zeyos/table.d.ts +205 -0
- package/dist/zx-compat.esm.js +375 -39
- package/dist/zx-compat.esm.min.js +1 -1
- package/dist/zx-compat.global.js +372 -73
- package/dist/zx-compat.global.min.js +3 -3
- package/dist/zx-zeyos.esm.js +355 -49
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +68 -6
- package/dist/zx.esm.js +359 -73
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +359 -73
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +48 -2
- package/docs/llms.md +31 -0
- package/package.json +37 -9
- package/src/compat/base.js +291 -0
- package/src/compat/globals.js +165 -0
- package/src/compat/index.js +84 -0
- package/src/compat/map/basic.js +464 -0
- package/src/compat/map/data.js +232 -0
- package/src/compat/map/date-time.js +177 -0
- package/src/compat/map/factory.js +45 -0
- package/src/compat/map/form.js +330 -0
- package/src/compat/map/helpers.js +46 -0
- package/src/compat/map/http.js +117 -0
- package/src/compat/map/layout.js +256 -0
- package/src/compat/map/options.js +267 -0
- package/src/compat/map/select.js +198 -0
- package/src/compat/map/stubs.js +26 -0
- package/src/compat/map/utilities.js +113 -0
- package/src/compat-entry.js +11 -0
- package/src/components/badge/badge.css +147 -0
- package/src/components/badge/badge.js +92 -0
- package/src/components/breadcrumb/breadcrumb.css +105 -0
- package/src/components/breadcrumb/breadcrumb.js +331 -0
- package/src/components/button/button.css +104 -0
- package/src/components/button/button.js +76 -0
- package/src/components/check-button/check-button.css +66 -0
- package/src/components/check-button/check-button.js +167 -0
- package/src/components/checklist/checklist.css +65 -0
- package/src/components/checklist/checklist.js +282 -0
- package/src/components/checklist/field-adapter.js +46 -0
- package/src/components/context-menu/context-menu.css +54 -0
- package/src/components/context-menu/context-menu.js +334 -0
- package/src/components/copy/copy-button.css +27 -0
- package/src/components/copy/copy-button.js +78 -0
- package/src/components/copy/copy-input.css +59 -0
- package/src/components/copy/copy-input.js +123 -0
- package/src/components/data-filter/data-filter.css +53 -0
- package/src/components/data-filter/data-filter.js +400 -0
- package/src/components/data-filter/filter-core.js +128 -0
- package/src/components/date-picker/date-picker.css +181 -0
- package/src/components/date-picker/date-picker.js +500 -0
- package/src/components/date-picker/date-range-picker.css +264 -0
- package/src/components/date-picker/date-range-picker.js +990 -0
- package/src/components/date-picker/field-adapter.js +36 -0
- package/src/components/date-picker/month-picker.js +223 -0
- package/src/components/date-picker/range-field-adapter.js +31 -0
- package/src/components/date-picker/time-picker.css +191 -0
- package/src/components/date-picker/time-picker.js +406 -0
- package/src/components/datebox/date-range-box.css +128 -0
- package/src/components/datebox/date-range-box.js +492 -0
- package/src/components/datebox/datebox.css +108 -0
- package/src/components/datebox/datebox.js +358 -0
- package/src/components/datebox/field-adapter.js +34 -0
- package/src/components/dialog/dialog.css +74 -0
- package/src/components/dialog/dialog.js +450 -0
- package/src/components/dropdown/dropdown.css +24 -0
- package/src/components/dropdown/dropdown.js +288 -0
- package/src/components/empty-state/empty-state.css +80 -0
- package/src/components/empty-state/empty-state.js +92 -0
- package/src/components/field/field.css +127 -0
- package/src/components/field/field.js +605 -0
- package/src/components/field-upload/field-adapter.js +21 -0
- package/src/components/field-upload/field-upload.css +108 -0
- package/src/components/field-upload/field-upload.js +475 -0
- package/src/components/fieldset/fieldset.css +37 -0
- package/src/components/fieldset/fieldset.js +307 -0
- package/src/components/finder/finder.css +98 -0
- package/src/components/finder/finder.js +509 -0
- package/src/components/form/form.css +32 -0
- package/src/components/form/form.js +302 -0
- package/src/components/groupbox/groupbox.css +51 -0
- package/src/components/groupbox/groupbox.js +199 -0
- package/src/components/layout/layout.css +117 -0
- package/src/components/layout/layout.js +136 -0
- package/src/components/loading/inline-loading.css +34 -0
- package/src/components/loading/inline-loading.js +140 -0
- package/src/components/loading/progress-bar.css +115 -0
- package/src/components/loading/progress-bar.js +259 -0
- package/src/components/loading/spinner.css +58 -0
- package/src/components/loading/spinner.js +57 -0
- package/src/components/master-panel/master-panel.css +51 -0
- package/src/components/master-panel/master-panel.js +203 -0
- package/src/components/menu-button/menu-button.css +49 -0
- package/src/components/menu-button/menu-button.js +294 -0
- package/src/components/message/message.css +122 -0
- package/src/components/message/message.js +499 -0
- package/src/components/modal/modal.css +47 -0
- package/src/components/modal/modal.js +203 -0
- package/src/components/multi-value-editor/field-adapter.js +44 -0
- package/src/components/multi-value-editor/multi-value-editor.css +56 -0
- package/src/components/multi-value-editor/multi-value-editor.js +170 -0
- package/src/components/navigation-bar/navigation-bar.css +130 -0
- package/src/components/navigation-bar/navigation-bar.js +450 -0
- package/src/components/number-field/field-adapter.js +40 -0
- package/src/components/number-field/number-field.css +77 -0
- package/src/components/number-field/number-field.js +545 -0
- package/src/components/pagination/pagination.css +95 -0
- package/src/components/pagination/pagination.js +481 -0
- package/src/components/panel/panel.css +123 -0
- package/src/components/panel/panel.js +336 -0
- package/src/components/permission/permission.css +47 -0
- package/src/components/permission/permission.js +198 -0
- package/src/components/rating/field-adapter.js +34 -0
- package/src/components/rating/rating.css +97 -0
- package/src/components/rating/rating.js +395 -0
- package/src/components/search/search.css +70 -0
- package/src/components/search/search.js +193 -0
- package/src/components/select/field-adapter.js +24 -0
- package/src/components/select/filter.js +39 -0
- package/src/components/select/select.css +180 -0
- package/src/components/select/select.js +663 -0
- package/src/components/skeleton/skeleton.css +76 -0
- package/src/components/skeleton/skeleton.js +139 -0
- package/src/components/slider/field-adapter.js +36 -0
- package/src/components/slider/slider.css +181 -0
- package/src/components/slider/slider.js +399 -0
- package/src/components/split-view/split-view.css +139 -0
- package/src/components/split-view/split-view.js +806 -0
- package/src/components/stepper/stepper.css +234 -0
- package/src/components/stepper/stepper.js +459 -0
- package/src/components/stepper/steps.js +131 -0
- package/src/components/tabbox/tabbox.css +281 -0
- package/src/components/tabbox/tabbox.js +484 -0
- package/src/components/table/sort.js +85 -0
- package/src/components/table/table.css +364 -0
- package/src/components/table/table.js +1711 -0
- package/src/components/tag-picker/field-adapter.js +35 -0
- package/src/components/tag-picker/tag-picker.css +169 -0
- package/src/components/tag-picker/tag-picker.js +837 -0
- package/src/components/timebox/field-adapter.js +24 -0
- package/src/components/timebox/timebox.css +51 -0
- package/src/components/timebox/timebox.js +276 -0
- package/src/components/toggle/field-adapter.js +24 -0
- package/src/components/toggle/toggle.css +66 -0
- package/src/components/toggle/toggle.js +150 -0
- package/src/components/toolbar/toolbar.css +81 -0
- package/src/components/toolbar/toolbar.js +511 -0
- package/src/components/tooltip/tooltip.css +36 -0
- package/src/components/tooltip/tooltip.js +420 -0
- package/src/components/tree/hierarchy.js +151 -0
- package/src/components/tree/tree.css +99 -0
- package/src/components/tree/tree.js +784 -0
- package/src/components/truncate/truncate.css +20 -0
- package/src/components/truncate/truncate.js +92 -0
- package/src/components/value-list/field-adapter.js +24 -0
- package/src/components/value-list/value-list.css +108 -0
- package/src/components/value-list/value-list.js +336 -0
- package/src/core/breakpoint.js +150 -0
- package/src/core/component.js +292 -0
- package/src/core/date.js +216 -0
- package/src/core/dom.js +260 -0
- package/src/core/export.js +204 -0
- package/src/core/fontawesome.js +177 -0
- package/src/core/format.js +283 -0
- package/src/core/http.js +246 -0
- package/src/core/i18n.js +83 -0
- package/src/core/icons.js +283 -0
- package/src/core/keyboard.js +240 -0
- package/src/core/position.js +177 -0
- package/src/core/storage.js +175 -0
- package/src/core/util.js +336 -0
- package/src/elements/define.js +715 -0
- package/src/elements/reflect.js +79 -0
- package/src/index.js +131 -0
- package/src/zeyos/connect.js +142 -0
- package/src/zeyos/form.js +254 -0
- package/src/zeyos/icons.js +92 -0
- package/src/zeyos/index.js +21 -0
- package/src/zeyos/modules.js +269 -0
- package/src/zeyos/query.js +199 -0
- package/src/zeyos/schema.js +474 -0
- package/src/zeyos/select.js +175 -0
- package/src/zeyos/table.js +272 -0
- package/styles/base.css +124 -0
- package/styles/icons.css +34 -0
- package/styles/tokens/dark.css +84 -0
- package/styles/tokens/density.css +9 -0
- package/styles/tokens/global.css +80 -0
- package/styles/tokens/modules.css +109 -0
- package/styles/tokens/semantic.css +49 -0
- package/styles/zx.css +61 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
import { Component } from '../../core/component.js';
|
|
2
|
+
import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
|
|
3
|
+
import { storage } from '../../core/storage.js';
|
|
4
|
+
import { uid } from '../../core/util.js';
|
|
5
|
+
|
|
6
|
+
/** Pixels an arrow key moves the divider. */
|
|
7
|
+
const STEP = 16;
|
|
8
|
+
/** Pixels a shifted arrow key moves the divider. */
|
|
9
|
+
const STEP_LARGE = 64;
|
|
10
|
+
/** Storage namespace shared by every split view that opted into persistence. */
|
|
11
|
+
const STORAGE_NAMESPACE = 'split-view';
|
|
12
|
+
|
|
13
|
+
/** @typedef {string|Node|Component|null} SplitViewContent */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} SplitViewOptions
|
|
17
|
+
* @property {'horizontal'|'vertical'} [orientation='horizontal'] Split axis. `horizontal` puts the
|
|
18
|
+
* panes side by side behind a vertical divider; `vertical` stacks them behind a horizontal one.
|
|
19
|
+
* @property {SplitViewContent} [start=null] Leading pane content (left, or top).
|
|
20
|
+
* @property {SplitViewContent} [end=null] Trailing pane content (right, or bottom).
|
|
21
|
+
* @property {number|string} [size='38%'] Initial size of the START pane. A number is pixels, a
|
|
22
|
+
* string is any CSS length the browser understands; `%` resolves against the container.
|
|
23
|
+
* @property {number} [min=160] Smallest the start pane may become, in pixels.
|
|
24
|
+
* @property {number|null} [max=null] Largest the start pane may become, in pixels; null means "as
|
|
25
|
+
* large as the container allows".
|
|
26
|
+
* @property {false|'start'|'end'} [collapsible=false] Which pane Enter/Space on the divider hides.
|
|
27
|
+
* @property {boolean|'start'|'end'} [collapsed=false] Whether a pane starts collapsed; `true` uses
|
|
28
|
+
* the `collapsible` pane.
|
|
29
|
+
* @property {number} [snap=0] Snap radius in pixels. A drag that ends within this distance of the
|
|
30
|
+
* initial size, the minimum, or the maximum lands exactly on it. `0` disables snapping.
|
|
31
|
+
* @property {string|null} [storageKey=null] Key under which the size is remembered between visits.
|
|
32
|
+
* Storage failures degrade to memory rather than throwing.
|
|
33
|
+
* @property {boolean} [disabled=false] Whether resizing is blocked.
|
|
34
|
+
* @property {string} [label='Resize panes'] Accessible name of the divider.
|
|
35
|
+
* @property {(event: CustomEvent<{size: number, ratio: number}>) => void} [onresize] Resize listener.
|
|
36
|
+
* @property {(event: CustomEvent<{size: number, ratio: number}>) => void} [onresizeend] Resize-end listener.
|
|
37
|
+
* @property {(event: CustomEvent<{pane: 'start'|'end'}>) => void} [oncollapse] Collapse listener.
|
|
38
|
+
* @property {(event: CustomEvent<Record<string, never>>) => void} [onexpand] Expand listener.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Two panes and a draggable divider between them.
|
|
43
|
+
*
|
|
44
|
+
* The layout is a three-track CSS grid driven by a single custom property, `--zx-split-size`, so a
|
|
45
|
+
* drag writes one style value per frame and the browser does the rest — no per-pane width
|
|
46
|
+
* arithmetic, no reflow storm. The property holds whatever was asked for: `38%` stays a percentage
|
|
47
|
+
* and keeps tracking the container until someone drags, at which point it becomes a pixel value
|
|
48
|
+
* and stays put.
|
|
49
|
+
*
|
|
50
|
+
* The divider follows the WAI-ARIA window splitter pattern: it is a focusable `separator` carrying
|
|
51
|
+
* live `aria-valuenow`/`aria-valuemin`/`aria-valuemax` in pixels, it moves with the arrow keys, and
|
|
52
|
+
* — where the component is `collapsible` — Enter or Space folds a pane away.
|
|
53
|
+
* @fires SplitView#resize
|
|
54
|
+
* @fires SplitView#resizeend
|
|
55
|
+
* @fires SplitView#collapse
|
|
56
|
+
* @fires SplitView#expand
|
|
57
|
+
* @extends {Component<SplitViewOptions>}
|
|
58
|
+
*/
|
|
59
|
+
export class SplitView extends Component {
|
|
60
|
+
static cssName = 'split-view';
|
|
61
|
+
|
|
62
|
+
/** @type {Readonly<SplitViewOptions>} */
|
|
63
|
+
static defaults = {
|
|
64
|
+
orientation: 'horizontal',
|
|
65
|
+
start: null,
|
|
66
|
+
end: null,
|
|
67
|
+
size: '38%',
|
|
68
|
+
min: 160,
|
|
69
|
+
max: null,
|
|
70
|
+
collapsible: false,
|
|
71
|
+
collapsed: false,
|
|
72
|
+
snap: 0,
|
|
73
|
+
storageKey: null,
|
|
74
|
+
disabled: false,
|
|
75
|
+
label: 'Resize panes'
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** @type {ResizeObserver|null} */
|
|
79
|
+
#observer = null;
|
|
80
|
+
/** @type {ReturnType<typeof storage>|null} */
|
|
81
|
+
#store = null;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Creates a split view, or turns an existing element into one.
|
|
85
|
+
* @param {Element|string|null} [target=null] Existing container, selector, or null to create one.
|
|
86
|
+
* @param {SplitViewOptions} [options={}] Split-view options.
|
|
87
|
+
*/
|
|
88
|
+
constructor(target = null, options = {}) {
|
|
89
|
+
super(target, options);
|
|
90
|
+
|
|
91
|
+
const key = this.options.storageKey;
|
|
92
|
+
if (key !== null && key !== undefined && key !== '') {
|
|
93
|
+
this.#store = storage(STORAGE_NAMESPACE);
|
|
94
|
+
const stored = this.#store.get(String(key));
|
|
95
|
+
if (typeof stored === 'number' && Number.isFinite(stored) && stored >= 0) this.setSize(stored);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const collapsed = this.options.collapsed;
|
|
99
|
+
if (collapsed === 'start' || collapsed === 'end') this.#setCollapsed(collapsed, false);
|
|
100
|
+
else if (collapsed === true && this.options.collapsible) {
|
|
101
|
+
this.#setCollapsed(this.options.collapsible === 'end' ? 'end' : 'start', false);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const divider = this.refs.divider;
|
|
105
|
+
this.listen(divider, 'pointerdown', (event) => this.#onPointerDown(/** @type {PointerEvent} */ (event)));
|
|
106
|
+
this.listen(divider, 'pointermove', (event) => this.#onPointerMove(/** @type {PointerEvent} */ (event)));
|
|
107
|
+
this.listen(divider, 'pointerup', (event) => this.#endDrag(/** @type {PointerEvent} */ (event)));
|
|
108
|
+
this.listen(divider, 'pointercancel', (event) => this.#endDrag(/** @type {PointerEvent} */ (event)));
|
|
109
|
+
this.listen(divider, 'keydown', (event) => this.#onKeyDown(/** @type {KeyboardEvent} */ (event)));
|
|
110
|
+
this.listen(divider, 'dblclick', () => this.#onDoubleClick());
|
|
111
|
+
|
|
112
|
+
if (typeof ResizeObserver === 'function') {
|
|
113
|
+
this.#observer = new ResizeObserver(() => this.#onContainerResize());
|
|
114
|
+
this.#observer.observe(this.el);
|
|
115
|
+
}
|
|
116
|
+
this.#syncAria();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Builds the panes and the divider. Runs inside the base constructor, before any class field
|
|
121
|
+
* exists, so every value it needs later is an ordinary instance property assigned here.
|
|
122
|
+
* @returns {HTMLElement}
|
|
123
|
+
*/
|
|
124
|
+
render() {
|
|
125
|
+
const options = /** @type {SplitViewOptions} */ (this.options);
|
|
126
|
+
this._orientation = options.orientation === 'vertical' ? 'vertical' : 'horizontal';
|
|
127
|
+
// A pixel size is known up front and can be clamped straight away; a percentage or any other
|
|
128
|
+
// CSS length stays null, which means "the property holds the option and CSS is in charge".
|
|
129
|
+
const initial = resolveSize(options.size, Number.NaN);
|
|
130
|
+
/** Current start size in pixels, or null while the CSS value in the property is in charge. */
|
|
131
|
+
this._size = Number.isFinite(initial) ? Math.max(0, initial) : null;
|
|
132
|
+
/** @type {'start'|'end'|null} */
|
|
133
|
+
this._collapsed = null;
|
|
134
|
+
this._disabled = Boolean(options.disabled);
|
|
135
|
+
this._destroyed = false;
|
|
136
|
+
this._dragging = false;
|
|
137
|
+
/** Handle of the frame a pending drag update is scheduled in; `0` if none. */
|
|
138
|
+
this._frame = 0;
|
|
139
|
+
/** @type {number|null} */
|
|
140
|
+
this._pointerId = null;
|
|
141
|
+
this._startClient = 0;
|
|
142
|
+
this._startSize = 0;
|
|
143
|
+
this._pointerClient = 0;
|
|
144
|
+
/** Whether the pointer left its starting position, which separates a drag from a click. */
|
|
145
|
+
this._moved = false;
|
|
146
|
+
/** Container size of the last observer pass; `-1` forces the next one to run. */
|
|
147
|
+
this._lastTotal = -1;
|
|
148
|
+
|
|
149
|
+
this._createdRoot = !this.el;
|
|
150
|
+
const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
|
|
151
|
+
this.el = root;
|
|
152
|
+
this._snapshot = this._createdRoot ? null : snapshotTarget(root);
|
|
153
|
+
// An enhanced element with no explicit pane options donates its first two children as panes.
|
|
154
|
+
const adopted = this._createdRoot || options.start != null || options.end != null
|
|
155
|
+
? []
|
|
156
|
+
: Array.from(root.children);
|
|
157
|
+
|
|
158
|
+
const startId = uid('zx-split-start');
|
|
159
|
+
const start = h('div', {
|
|
160
|
+
class: 'zx-split-view__pane', id: startId, ref: 'start', dataset: { pane: 'start' }
|
|
161
|
+
});
|
|
162
|
+
const divider = h('div', {
|
|
163
|
+
class: 'zx-split-view__divider',
|
|
164
|
+
ref: 'divider',
|
|
165
|
+
role: 'separator',
|
|
166
|
+
tabindex: this._disabled ? -1 : 0,
|
|
167
|
+
// A separator's aria-orientation describes the separator itself, not the split: panes lying
|
|
168
|
+
// side by side (orientation "horizontal") are parted by a vertical line.
|
|
169
|
+
ariaOrientation: this._orientation === 'vertical' ? 'horizontal' : 'vertical',
|
|
170
|
+
ariaLabel: String(options.label ?? 'Resize panes'),
|
|
171
|
+
ariaControls: startId
|
|
172
|
+
});
|
|
173
|
+
const end = h('div', {
|
|
174
|
+
class: 'zx-split-view__pane', ref: 'end', dataset: { pane: 'end' }
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
root.replaceChildren(start, divider, end);
|
|
178
|
+
root.dataset.orientation = this._orientation;
|
|
179
|
+
if (this._disabled) divider.setAttribute('aria-disabled', 'true');
|
|
180
|
+
root.style.setProperty('--zx-split-size', cssLength(options.size));
|
|
181
|
+
|
|
182
|
+
if (options.start != null) replacePaneContent(start, options.start, 'SplitView start');
|
|
183
|
+
else if (adopted[0]) start.append(adopted[0]);
|
|
184
|
+
if (options.end != null) replacePaneContent(end, options.end, 'SplitView end');
|
|
185
|
+
else if (adopted[1]) end.append(...adopted.slice(1));
|
|
186
|
+
|
|
187
|
+
return root;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Sets the start pane size. Pixels and percentages are clamped against `min`, `max`, and the
|
|
192
|
+
* container immediately; any other CSS length is handed to the browser unclamped. Programmatic,
|
|
193
|
+
* so it emits nothing.
|
|
194
|
+
* @param {number|string} value Pixel number or CSS length.
|
|
195
|
+
* @returns {this}
|
|
196
|
+
*/
|
|
197
|
+
setSize(value) {
|
|
198
|
+
const { total, divider, min, max } = this.#metrics();
|
|
199
|
+
// With no container to measure, a percentage means nothing yet — leave it to CSS rather than
|
|
200
|
+
// resolving it to zero and pinning the pane shut.
|
|
201
|
+
const parsed = resolveSize(value, total > 0 ? total : Number.NaN);
|
|
202
|
+
if (Number.isFinite(parsed)) {
|
|
203
|
+
this.#applySize(clampSize({ size: parsed, min, max, total, divider }));
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
206
|
+
this._size = null;
|
|
207
|
+
/** @type {HTMLElement} */ (this.el).style.setProperty('--zx-split-size', cssLength(value));
|
|
208
|
+
this.#syncAria();
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Returns the start pane's current size in pixels: `0` while the start pane is collapsed, and
|
|
214
|
+
* everything but the divider while the end pane is.
|
|
215
|
+
* @returns {number}
|
|
216
|
+
*/
|
|
217
|
+
getSize() {
|
|
218
|
+
if (this._collapsed === 'start') return 0;
|
|
219
|
+
const { total, divider } = this.#metrics();
|
|
220
|
+
if (this._collapsed === 'end') return Math.max(0, total - divider);
|
|
221
|
+
if (this._size !== null) return this._size;
|
|
222
|
+
// A CSS-driven size is whatever the browser laid out, so measure rather than guess.
|
|
223
|
+
const measured = paneExtent(this.refs.start, this._orientation);
|
|
224
|
+
if (measured > 0) return measured;
|
|
225
|
+
const parsed = resolveSize(this.options.size, total);
|
|
226
|
+
return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Returns the start pane's share of the container, between 0 and 1.
|
|
231
|
+
* @returns {number}
|
|
232
|
+
*/
|
|
233
|
+
getRatio() {
|
|
234
|
+
const { total } = this.#metrics();
|
|
235
|
+
return total > 0 ? this.getSize() / total : 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Replaces the start pane's content.
|
|
240
|
+
* @param {SplitViewContent} content Text, node, component, or null to empty the pane.
|
|
241
|
+
* @returns {this}
|
|
242
|
+
*/
|
|
243
|
+
setStart(content) {
|
|
244
|
+
replacePaneContent(this.refs.start, content, 'SplitView start');
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Replaces the end pane's content.
|
|
250
|
+
* @param {SplitViewContent} content Text, node, component, or null to empty the pane.
|
|
251
|
+
* @returns {this}
|
|
252
|
+
*/
|
|
253
|
+
setEnd(content) {
|
|
254
|
+
replacePaneContent(this.refs.end, content, 'SplitView end');
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Folds one pane away, keeping the remembered size for `expand()`. Works whatever `collapsible`
|
|
260
|
+
* says — that option only governs the keyboard affordance.
|
|
261
|
+
* @param {'start'|'end'} [which] Pane to hide; defaults to the `collapsible` pane, else `start`.
|
|
262
|
+
* @returns {this}
|
|
263
|
+
* @fires SplitView#collapse
|
|
264
|
+
*/
|
|
265
|
+
collapse(which) {
|
|
266
|
+
const fallback = this.options.collapsible === 'end' ? 'end' : 'start';
|
|
267
|
+
return this.#setCollapsed(which === 'end' ? 'end' : (which === 'start' ? 'start' : fallback), true);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Restores a collapsed pane to the size it had.
|
|
272
|
+
* @returns {this}
|
|
273
|
+
* @fires SplitView#expand
|
|
274
|
+
*/
|
|
275
|
+
expand() {
|
|
276
|
+
if (this._collapsed === null) return this;
|
|
277
|
+
this._collapsed = null;
|
|
278
|
+
delete /** @type {HTMLElement} */ (this.el).dataset.collapsed;
|
|
279
|
+
this.#syncAria();
|
|
280
|
+
this.emit('expand');
|
|
281
|
+
return this;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Reports which pane is collapsed, if any.
|
|
286
|
+
* @returns {'start'|'end'|false}
|
|
287
|
+
*/
|
|
288
|
+
isCollapsed() {
|
|
289
|
+
return this._collapsed ?? false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Allows resizing again and puts the divider back in the tab order.
|
|
294
|
+
* @returns {this}
|
|
295
|
+
*/
|
|
296
|
+
enable() {
|
|
297
|
+
this._disabled = false;
|
|
298
|
+
this.refs.divider.removeAttribute('aria-disabled');
|
|
299
|
+
this.refs.divider.setAttribute('tabindex', '0');
|
|
300
|
+
return this;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Blocks pointer and keyboard resizing and takes the divider out of the tab order. Any drag in
|
|
305
|
+
* flight ends immediately.
|
|
306
|
+
* @returns {this}
|
|
307
|
+
*/
|
|
308
|
+
disable() {
|
|
309
|
+
this._disabled = true;
|
|
310
|
+
this.#endDrag(null);
|
|
311
|
+
this.refs.divider.setAttribute('aria-disabled', 'true');
|
|
312
|
+
this.refs.divider.setAttribute('tabindex', '-1');
|
|
313
|
+
return this;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Reports whether resizing is blocked.
|
|
318
|
+
* @returns {boolean}
|
|
319
|
+
*/
|
|
320
|
+
isDisabled() {
|
|
321
|
+
return this._disabled;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Disconnects the observer, drops listeners, and restores an enhanced target's original content.
|
|
326
|
+
* @returns {void}
|
|
327
|
+
*/
|
|
328
|
+
destroy() {
|
|
329
|
+
if (this._destroyed) return;
|
|
330
|
+
this._destroyed = true;
|
|
331
|
+
if (this._frame !== 0) {
|
|
332
|
+
cancelAnimationFrame(this._frame);
|
|
333
|
+
this._frame = 0;
|
|
334
|
+
}
|
|
335
|
+
this.#releasePointer();
|
|
336
|
+
this.#observer?.disconnect();
|
|
337
|
+
this.#observer = null;
|
|
338
|
+
super.destroy();
|
|
339
|
+
if (!this._createdRoot && this._snapshot) restoreTarget(this.el, this._snapshot);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Collects everything the geometry helpers need, in pixels.
|
|
344
|
+
* @returns {{total: number, divider: number, min: number, max: number|null}}
|
|
345
|
+
*/
|
|
346
|
+
#metrics() {
|
|
347
|
+
const root = /** @type {HTMLElement} */ (this.el);
|
|
348
|
+
const total = this._orientation === 'vertical' ? root.clientHeight : root.clientWidth;
|
|
349
|
+
const max = this.options.max == null ? null : Number(this.options.max);
|
|
350
|
+
return {
|
|
351
|
+
total: Number.isFinite(total) && total > 0 ? total : 0,
|
|
352
|
+
divider: paneExtent(this.refs.divider, this._orientation),
|
|
353
|
+
min: Math.max(0, Number(this.options.min) || 0),
|
|
354
|
+
max: Number.isFinite(/** @type {number} */ (max)) ? max : null
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Writes a pixel size to the one property the grid reads, and mirrors it into the divider's ARIA.
|
|
360
|
+
* @param {number} size Clamped pixel size.
|
|
361
|
+
* @returns {void}
|
|
362
|
+
*/
|
|
363
|
+
#applySize(size) {
|
|
364
|
+
this._size = size;
|
|
365
|
+
/** @type {HTMLElement} */ (this.el).style.setProperty('--zx-split-size', `${size}px`);
|
|
366
|
+
this.#syncAria();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** @returns {void} */
|
|
370
|
+
#syncAria() {
|
|
371
|
+
const { total, divider, min, max } = this.#metrics();
|
|
372
|
+
const bounds = sizeBounds({ min, max, total, divider });
|
|
373
|
+
const element = this.refs.divider;
|
|
374
|
+
element.setAttribute('aria-valuemin', String(Math.round(bounds.min)));
|
|
375
|
+
element.setAttribute('aria-valuemax', String(Math.round(bounds.max)));
|
|
376
|
+
element.setAttribute('aria-valuenow', String(Math.round(this.getSize())));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* @param {'start'|'end'} pane
|
|
381
|
+
* @param {boolean} notify
|
|
382
|
+
* @returns {this}
|
|
383
|
+
*/
|
|
384
|
+
#setCollapsed(pane, notify) {
|
|
385
|
+
if (this._collapsed === pane) return this;
|
|
386
|
+
this._collapsed = pane;
|
|
387
|
+
/** @type {HTMLElement} */ (this.el).dataset.collapsed = pane;
|
|
388
|
+
this.#syncAria();
|
|
389
|
+
if (notify) this.emit('collapse', { pane });
|
|
390
|
+
return this;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** @param {PointerEvent} event @returns {void} */
|
|
394
|
+
#onPointerDown(event) {
|
|
395
|
+
if (this._disabled || this._dragging || event.button !== 0) return;
|
|
396
|
+
// No preventDefault(): cancelling pointerdown suppresses the compatibility mouse events the
|
|
397
|
+
// double-click reset is built on. `user-select: none` on the divider stops the text selection
|
|
398
|
+
// this would otherwise be guarding against.
|
|
399
|
+
this._dragging = true;
|
|
400
|
+
this._pointerId = event.pointerId;
|
|
401
|
+
this._startClient = axisClient(event, this._orientation);
|
|
402
|
+
this._pointerClient = this._startClient;
|
|
403
|
+
this._startSize = this.getSize();
|
|
404
|
+
this._moved = false;
|
|
405
|
+
/** @type {HTMLElement} */ (this.el).dataset.dragging = 'true';
|
|
406
|
+
try {
|
|
407
|
+
this.refs.divider.setPointerCapture(event.pointerId);
|
|
408
|
+
} catch {
|
|
409
|
+
// A pointer that ended between the event and this call cannot be captured; the drag simply
|
|
410
|
+
// finishes on the next pointerup.
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** @param {PointerEvent} event @returns {void} */
|
|
415
|
+
#onPointerMove(event) {
|
|
416
|
+
if (!this._dragging || event.pointerId !== this._pointerId) return;
|
|
417
|
+
this._pointerClient = axisClient(event, this._orientation);
|
|
418
|
+
if (this._pointerClient !== this._startClient) this._moved = true;
|
|
419
|
+
if (this._frame !== 0) return;
|
|
420
|
+
// One write and at most one `resize` per frame, however fast the pointer reports.
|
|
421
|
+
this._frame = requestAnimationFrame(() => {
|
|
422
|
+
this._frame = 0;
|
|
423
|
+
this.#trackPointer(false);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Clamps the pointer position into a legal size and applies it.
|
|
429
|
+
* @param {boolean} settle Whether to apply the snap radius, which only makes sense at drag end.
|
|
430
|
+
* @returns {void}
|
|
431
|
+
*/
|
|
432
|
+
#trackPointer(settle) {
|
|
433
|
+
if (!this._dragging || this._destroyed) return;
|
|
434
|
+
const { total, divider, min, max } = this.#metrics();
|
|
435
|
+
if (total <= 0) return;
|
|
436
|
+
// Dragging a folded pane brings it back rather than doing nothing; a plain click does not,
|
|
437
|
+
// because this runs only once the pointer has actually moved.
|
|
438
|
+
if (this._collapsed !== null) this.expand();
|
|
439
|
+
let next = clampSize({
|
|
440
|
+
size: this._startSize + (this._pointerClient - this._startClient), min, max, total, divider
|
|
441
|
+
});
|
|
442
|
+
if (settle) {
|
|
443
|
+
next = clampSize({
|
|
444
|
+
size: snapSize(next, this.#snapTargets(total, divider), Number(this.options.snap)),
|
|
445
|
+
min, max, total, divider
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
if (next === this._size) return;
|
|
449
|
+
this.#applySize(next);
|
|
450
|
+
this.emit('resize', { size: next, ratio: next / total });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* @param {number} total
|
|
455
|
+
* @param {number} divider
|
|
456
|
+
* @returns {number[]}
|
|
457
|
+
*/
|
|
458
|
+
#snapTargets(total, divider) {
|
|
459
|
+
const { min, max } = this.#metrics();
|
|
460
|
+
const bounds = sizeBounds({ min, max, total, divider });
|
|
461
|
+
const targets = [bounds.min, bounds.max];
|
|
462
|
+
const initial = resolveSize(this.options.size, total);
|
|
463
|
+
if (Number.isFinite(initial)) targets.unshift(initial);
|
|
464
|
+
return targets;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Finishes a drag: flushes the last frame, applies the snap radius, releases the capture,
|
|
469
|
+
* persists, and reports. A press that never moved is a click, not a resize, and reports nothing.
|
|
470
|
+
* @param {PointerEvent|null} event Ending pointer event, or null when ending programmatically.
|
|
471
|
+
* @returns {void}
|
|
472
|
+
* @fires SplitView#resizeend
|
|
473
|
+
*/
|
|
474
|
+
#endDrag(event) {
|
|
475
|
+
if (!this._dragging) return;
|
|
476
|
+
if (event && event.pointerId !== this._pointerId) return;
|
|
477
|
+
if (this._frame !== 0) {
|
|
478
|
+
cancelAnimationFrame(this._frame);
|
|
479
|
+
this._frame = 0;
|
|
480
|
+
}
|
|
481
|
+
if (event) this._pointerClient = axisClient(event, this._orientation);
|
|
482
|
+
const moved = this._moved;
|
|
483
|
+
// A drag that ends back where it started still has a final position to restore and report.
|
|
484
|
+
if (moved) this.#trackPointer(true);
|
|
485
|
+
this._dragging = false;
|
|
486
|
+
this._moved = false;
|
|
487
|
+
this.#releasePointer();
|
|
488
|
+
delete /** @type {HTMLElement} */ (this.el).dataset.dragging;
|
|
489
|
+
if (!moved) return;
|
|
490
|
+
this.#persist();
|
|
491
|
+
this.#emitResizeEnd();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** @returns {void} */
|
|
495
|
+
#releasePointer() {
|
|
496
|
+
const pointerId = this._pointerId;
|
|
497
|
+
this._pointerId = null;
|
|
498
|
+
if (pointerId === null) return;
|
|
499
|
+
try {
|
|
500
|
+
this.refs.divider.releasePointerCapture(pointerId);
|
|
501
|
+
} catch {
|
|
502
|
+
// The capture is already gone once the pointer was cancelled or the node was detached.
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** @param {KeyboardEvent} event @returns {void} */
|
|
507
|
+
#onKeyDown(event) {
|
|
508
|
+
if (this._disabled) return;
|
|
509
|
+
const key = event.key;
|
|
510
|
+
|
|
511
|
+
if (key === 'Enter' || key === ' ') {
|
|
512
|
+
if (!this.options.collapsible) return;
|
|
513
|
+
event.preventDefault();
|
|
514
|
+
if (this._collapsed === null) {
|
|
515
|
+
this.collapse(/** @type {'start'|'end'} */ (this.options.collapsible));
|
|
516
|
+
} else {
|
|
517
|
+
this.expand();
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const horizontal = this._orientation === 'horizontal';
|
|
523
|
+
const shrink = horizontal ? 'ArrowLeft' : 'ArrowUp';
|
|
524
|
+
const grow = horizontal ? 'ArrowRight' : 'ArrowDown';
|
|
525
|
+
if (key !== shrink && key !== grow && key !== 'Home' && key !== 'End') return;
|
|
526
|
+
event.preventDefault();
|
|
527
|
+
// A key that moves the divider implies the pane should be visible again.
|
|
528
|
+
if (this._collapsed !== null) this.expand();
|
|
529
|
+
|
|
530
|
+
const { total, divider, min, max } = this.#metrics();
|
|
531
|
+
if (total <= 0) return;
|
|
532
|
+
const bounds = sizeBounds({ min, max, total, divider });
|
|
533
|
+
const step = event.shiftKey ? STEP_LARGE : STEP;
|
|
534
|
+
let next = bounds.min;
|
|
535
|
+
if (key === 'End') next = bounds.max;
|
|
536
|
+
else if (key !== 'Home') next = this.getSize() + (key === grow ? step : -step);
|
|
537
|
+
|
|
538
|
+
const size = clampSize({ size: next, min, max, total, divider });
|
|
539
|
+
if (size === this._size) return;
|
|
540
|
+
this.#applySize(size);
|
|
541
|
+
this.emit('resize', { size, ratio: size / total });
|
|
542
|
+
this.#persist();
|
|
543
|
+
this.#emitResizeEnd();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Resets the divider to the initial `size`. A percentage goes back to being a percentage rather
|
|
548
|
+
* than to the pixel value it happens to work out at, so the pane keeps tracking the container.
|
|
549
|
+
* @returns {void}
|
|
550
|
+
* @fires SplitView#resize
|
|
551
|
+
* @fires SplitView#resizeend
|
|
552
|
+
*/
|
|
553
|
+
#onDoubleClick() {
|
|
554
|
+
if (this._disabled) return;
|
|
555
|
+
if (this._collapsed !== null) this.expand();
|
|
556
|
+
const initial = resolveSize(this.options.size, Number.NaN);
|
|
557
|
+
if (Number.isFinite(initial)) {
|
|
558
|
+
this.setSize(initial);
|
|
559
|
+
} else {
|
|
560
|
+
this._size = null;
|
|
561
|
+
/** @type {HTMLElement} */ (this.el).style.setProperty('--zx-split-size', cssLength(this.options.size));
|
|
562
|
+
this._lastTotal = -1;
|
|
563
|
+
this.#onContainerResize();
|
|
564
|
+
}
|
|
565
|
+
const { total } = this.#metrics();
|
|
566
|
+
const size = this.getSize();
|
|
567
|
+
this.emit('resize', { size, ratio: total > 0 ? size / total : 0 });
|
|
568
|
+
this.#persist();
|
|
569
|
+
this.#emitResizeEnd();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Re-clamps after the container changed size.
|
|
574
|
+
*
|
|
575
|
+
* The pass bails on a container that measures zero — a hidden tab or a detached node — because
|
|
576
|
+
* clamping against nothing would collapse a perfectly good size to the minimum and then persist
|
|
577
|
+
* it. It also returns early when the container size is unchanged, and writes only when clamping
|
|
578
|
+
* actually moved the value, so the writes it makes can never drive the observer in a loop.
|
|
579
|
+
*
|
|
580
|
+
* A percentage keeps its responsiveness: it is re-checked against the bounds on every container
|
|
581
|
+
* change and only pinned to pixels for as long as it actually breaks one, so a container that
|
|
582
|
+
* grows back gets its percentage back too.
|
|
583
|
+
* @returns {void}
|
|
584
|
+
*/
|
|
585
|
+
#onContainerResize() {
|
|
586
|
+
if (this._destroyed || this._dragging) return;
|
|
587
|
+
const { total, divider, min, max } = this.#metrics();
|
|
588
|
+
if (total <= 0 || total === this._lastTotal) return;
|
|
589
|
+
this._lastTotal = total;
|
|
590
|
+
|
|
591
|
+
if (this._size === null) {
|
|
592
|
+
const resolved = resolveSize(this.options.size, total);
|
|
593
|
+
if (Number.isFinite(resolved)) {
|
|
594
|
+
const clamped = clampSize({ size: resolved, min, max, total, divider });
|
|
595
|
+
/** @type {HTMLElement} */ (this.el).style.setProperty('--zx-split-size',
|
|
596
|
+
Math.abs(clamped - resolved) > 0.5 ? `${clamped}px` : cssLength(this.options.size));
|
|
597
|
+
}
|
|
598
|
+
this.#syncAria();
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const clamped = clampSize({ size: this._size, min, max, total, divider });
|
|
603
|
+
if (clamped === this._size) {
|
|
604
|
+
this.#syncAria();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
this.#applySize(clamped);
|
|
608
|
+
this.emit('resize', { size: clamped, ratio: clamped / total });
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Remembers the current pixel size, or forgets a remembered one once the size went back to
|
|
613
|
+
* being CSS-driven — otherwise a reset would be undone by the next visit.
|
|
614
|
+
* @returns {void}
|
|
615
|
+
*/
|
|
616
|
+
#persist() {
|
|
617
|
+
const key = this.options.storageKey;
|
|
618
|
+
if (!this.#store || key == null || key === '') return;
|
|
619
|
+
if (this._size === null) this.#store.remove(String(key));
|
|
620
|
+
else this.#store.set(String(key), Math.round(this._size));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** @returns {void} @fires SplitView#resizeend */
|
|
624
|
+
#emitResizeEnd() {
|
|
625
|
+
const { total } = this.#metrics();
|
|
626
|
+
const size = this.getSize();
|
|
627
|
+
this.emit('resizeend', { size, ratio: total > 0 ? size / total : 0 });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Fired while the divider moves, at most once per animation frame during a drag.
|
|
633
|
+
* @event SplitView#resize
|
|
634
|
+
* @type {CustomEvent<{size: number, ratio: number}>}
|
|
635
|
+
*/
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Fired once an interaction settles: a drag ends, a key moves the divider, or a double-click
|
|
639
|
+
* resets it.
|
|
640
|
+
* @event SplitView#resizeend
|
|
641
|
+
* @type {CustomEvent<{size: number, ratio: number}>}
|
|
642
|
+
*/
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Fired when a pane folds away.
|
|
646
|
+
* @event SplitView#collapse
|
|
647
|
+
* @type {CustomEvent<{pane: 'start'|'end'}>}
|
|
648
|
+
*/
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Fired when a collapsed pane comes back.
|
|
652
|
+
* @event SplitView#expand
|
|
653
|
+
* @type {CustomEvent<Record<string, never>>}
|
|
654
|
+
*/
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* @typedef {Object} SizeBounds
|
|
658
|
+
* @property {number} min Smallest legal start size in pixels.
|
|
659
|
+
* @property {number} max Largest legal start size in pixels.
|
|
660
|
+
*/
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* @typedef {Object} ClampSizeInput
|
|
664
|
+
* @property {number} size Requested start size in pixels.
|
|
665
|
+
* @property {number} [min=0] Smallest start size in pixels.
|
|
666
|
+
* @property {number|null} [max=null] Largest start size in pixels, or null for "whatever fits".
|
|
667
|
+
* @property {number} [total=0] Container size along the split axis, in pixels.
|
|
668
|
+
* @property {number} [divider=0] Divider thickness, in pixels.
|
|
669
|
+
*/
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Restricts a start-pane size to what the container can actually give it.
|
|
673
|
+
*
|
|
674
|
+
* The upper bound is never more than the space left after the divider, so the trailing pane can
|
|
675
|
+
* never be pushed to a negative size; when the container is too small even for `min`, the minimum
|
|
676
|
+
* yields rather than overflowing. A container that measures zero or less — a hidden tab, a node
|
|
677
|
+
* that is not in the document — has nothing to clamp against, so the requested size passes through
|
|
678
|
+
* untouched instead of being flattened to zero.
|
|
679
|
+
* @param {ClampSizeInput} input Requested size and the geometry around it.
|
|
680
|
+
* @returns {number} A size in pixels, never negative.
|
|
681
|
+
*/
|
|
682
|
+
export function clampSize({ size, min = 0, max = null, total = 0, divider = 0 }) {
|
|
683
|
+
const requested = Number(size);
|
|
684
|
+
const bounds = sizeBounds({ min, max, total, divider });
|
|
685
|
+
const available = Math.max(0, (Number(total) || 0) - Math.max(0, Number(divider) || 0));
|
|
686
|
+
if (!(available > 0)) return Number.isFinite(requested) ? Math.max(0, requested) : bounds.min;
|
|
687
|
+
if (!Number.isFinite(requested)) return bounds.min;
|
|
688
|
+
return Math.min(bounds.max, Math.max(bounds.min, requested));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Resolves a size option to pixels without touching the DOM.
|
|
693
|
+
*
|
|
694
|
+
* Numbers, bare numeric strings, and `px` lengths are pixels; a percentage resolves against
|
|
695
|
+
* `total`. Every other CSS length — `rem`, `vh`, `calc()` — needs the browser, so it returns `NaN`
|
|
696
|
+
* and the caller hands the value to CSS instead of guessing at it.
|
|
697
|
+
* @param {number|string} value Size option.
|
|
698
|
+
* @param {number} total Container size along the split axis, in pixels.
|
|
699
|
+
* @returns {number} Pixels, or `NaN` when only the browser can resolve the value.
|
|
700
|
+
*/
|
|
701
|
+
export function resolveSize(value, total) {
|
|
702
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : Number.NaN;
|
|
703
|
+
if (typeof value !== 'string') return Number.NaN;
|
|
704
|
+
const match = /^([+-]?(?:\d+\.?\d*|\.\d+))(px|%)?$/i.exec(value.trim());
|
|
705
|
+
if (!match) return Number.NaN;
|
|
706
|
+
const amount = Number(match[1]);
|
|
707
|
+
if (!Number.isFinite(amount)) return Number.NaN;
|
|
708
|
+
if ((match[2] ?? '').toLowerCase() !== '%') return amount;
|
|
709
|
+
const span = Number(total);
|
|
710
|
+
return Number.isFinite(span) ? (amount / 100) * span : Number.NaN;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Pulls a size onto the nearest target within a radius, leaving it alone when nothing is close.
|
|
715
|
+
* The radius is inclusive, ties go to the target listed first, and a radius of zero or less turns
|
|
716
|
+
* snapping off.
|
|
717
|
+
* @param {number} size Size in pixels.
|
|
718
|
+
* @param {number[]|number|null|undefined} targets Sizes worth landing exactly on.
|
|
719
|
+
* @param {number} threshold Snap radius in pixels.
|
|
720
|
+
* @returns {number}
|
|
721
|
+
*/
|
|
722
|
+
export function snapSize(size, targets, threshold) {
|
|
723
|
+
const value = Number(size);
|
|
724
|
+
const radius = Number(threshold);
|
|
725
|
+
if (!Number.isFinite(value) || !Number.isFinite(radius) || radius <= 0) return value;
|
|
726
|
+
|
|
727
|
+
const list = Array.isArray(targets) ? targets : (targets == null ? [] : [targets]);
|
|
728
|
+
let best = value;
|
|
729
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
730
|
+
for (const candidate of list) {
|
|
731
|
+
const target = Number(candidate);
|
|
732
|
+
if (!Number.isFinite(target)) continue;
|
|
733
|
+
const distance = Math.abs(target - value);
|
|
734
|
+
if (distance <= radius && distance < bestDistance) {
|
|
735
|
+
bestDistance = distance;
|
|
736
|
+
best = target;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return best;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Derives the legal size range from the container and the options.
|
|
744
|
+
* @param {{min?: number, max?: number|null, total?: number, divider?: number}} input Geometry.
|
|
745
|
+
* @returns {SizeBounds}
|
|
746
|
+
*/
|
|
747
|
+
function sizeBounds({ min = 0, max = null, total = 0, divider = 0 }) {
|
|
748
|
+
const available = Math.max(0, (Number(total) || 0) - Math.max(0, Number(divider) || 0));
|
|
749
|
+
const upper = max == null || !Number.isFinite(Number(max))
|
|
750
|
+
? available
|
|
751
|
+
: Math.min(available, Math.max(0, Number(max)));
|
|
752
|
+
const lower = Math.min(Math.max(0, Number(min) || 0), upper);
|
|
753
|
+
return { min: lower, max: upper };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Measures an element along the split axis.
|
|
758
|
+
* @param {Element} element Pane or divider.
|
|
759
|
+
* @param {'horizontal'|'vertical'} orientation Split axis.
|
|
760
|
+
* @returns {number}
|
|
761
|
+
*/
|
|
762
|
+
function paneExtent(element, orientation) {
|
|
763
|
+
const rect = element.getBoundingClientRect();
|
|
764
|
+
const value = orientation === 'vertical' ? rect.height : rect.width;
|
|
765
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Reads the pointer coordinate that matters for the split axis.
|
|
770
|
+
* @param {PointerEvent} event Pointer event.
|
|
771
|
+
* @param {'horizontal'|'vertical'} orientation Split axis.
|
|
772
|
+
* @returns {number}
|
|
773
|
+
*/
|
|
774
|
+
function axisClient(event, orientation) {
|
|
775
|
+
return orientation === 'vertical' ? event.clientY : event.clientX;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Normalizes a length option: a number is pixels, a string is passed through to CSS.
|
|
780
|
+
* @param {number|string} value Length option.
|
|
781
|
+
* @returns {string}
|
|
782
|
+
*/
|
|
783
|
+
function cssLength(value) {
|
|
784
|
+
return typeof value === 'number' && Number.isFinite(value) ? `${value}px` : String(value);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* @param {Element} pane Target pane.
|
|
789
|
+
* @param {SplitViewContent} content Replacement content.
|
|
790
|
+
* @param {string} label Name used in the type error.
|
|
791
|
+
* @returns {void}
|
|
792
|
+
*/
|
|
793
|
+
function replacePaneContent(pane, content, label) {
|
|
794
|
+
pane.replaceChildren();
|
|
795
|
+
if (content === null || content === undefined) return;
|
|
796
|
+
if (typeof content === 'string' || typeof content === 'number') {
|
|
797
|
+
pane.append(document.createTextNode(String(content)));
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const node = content instanceof Component ? content.toElement() : content;
|
|
801
|
+
if (node && typeof (/** @type {any} */ (node).nodeType) === 'number') {
|
|
802
|
+
pane.append(/** @type {Node} */ (node));
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
throw new TypeError(`${label} must be a string, Node, Component, or null`);
|
|
806
|
+
}
|