@kubex/zinc 1.1.185 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.browserslistrc +2 -0
- package/.github/workflows/js_build_and_deploy.yaml +54 -1
- package/.github/workflows/test.yaml +27 -0
- package/dist/chunks/zn.UWJLIXDT.js +1 -0
- package/dist/custom-elements.json +9998 -2345
- package/dist/vscode.html-custom-data.json +1022 -67
- package/dist/web-types.json +2129 -177
- package/dist/zn.d.ts +2293 -416
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +2310 -1637
- package/docs/_includes/default.njk +4 -1
- package/docs/assets/scripts/docs.js +22 -6
- package/docs/assets/styles/docs.css +9 -2
- package/docs/data/report-table-day-compare.json +380 -0
- package/docs/data/report-table.json +314 -0
- package/docs/pages/components/avatar-group.md +57 -0
- package/docs/pages/components/badge.md +67 -0
- package/docs/pages/components/button.md +41 -1
- package/docs/pages/components/chat-message.md +16 -0
- package/docs/pages/components/checkbox.md +1 -1
- package/docs/pages/components/chip.md +25 -13
- package/docs/pages/components/color-picker.md +37 -0
- package/docs/pages/components/copy-button.md +2 -2
- package/docs/pages/components/data-table-filter.md +33 -50
- package/docs/pages/components/data-table-search.md +4 -4
- package/docs/pages/components/data-table.md +27 -3
- package/docs/pages/components/date-range.md +116 -0
- package/docs/pages/components/dialog.md +29 -37
- package/docs/pages/components/diff-row.md +28 -0
- package/docs/pages/components/diff.md +46 -0
- package/docs/pages/components/editor.md +55 -0
- package/docs/pages/components/icon.md +46 -1
- package/docs/pages/components/input.md +30 -11
- package/docs/pages/components/item.md +35 -0
- package/docs/pages/components/key-value.md +25 -0
- package/docs/pages/components/menu.md +74 -7
- package/docs/pages/components/nav-item.md +15 -0
- package/docs/pages/components/nav-list.md +132 -0
- package/docs/pages/components/navbar.md +100 -1
- package/docs/pages/components/page.md +242 -74
- package/docs/pages/components/priority-list.md +43 -5
- package/docs/pages/components/query-builder.md +23 -0
- package/docs/pages/components/radio-group.md +33 -1
- package/docs/pages/components/radio.md +1 -1
- package/docs/pages/components/report-table.md +202 -0
- package/docs/pages/components/stat.md +30 -0
- package/docs/pages/components/tile.md +11 -0
- package/docs/pages/components/tooltip.md +1 -1
- package/docs/pages/components/translation-grid.md +86 -0
- package/docs/pages/components/vertical-stepper.md +108 -64
- package/package.json +2 -1
- package/scss/_global-spacing.scss +2 -2
- package/scss/_root.scss +11 -171
- package/scss/boot.scss +14 -0
- package/scss/mixins/_helper-mixins.scss +24 -2
- package/scss/mixins/_motion-mixins.scss +126 -0
- package/scss/mixins/_spacing-mixins.scss +4 -3
- package/scss/mixins/_tab-indicator-mixins.scss +12 -0
- package/scss/mixins/_typography-mixins.scss +1 -1
- package/scss/mixins/index.scss +14 -0
- package/scss/shared/_base.scss +32 -1
- package/scss/shared/_button-mixin.scss +7 -5
- package/scss/shared/_table.scss +35 -4
- package/scss/shared/nav.scss +4 -0
- package/scss/shared/shimmer.scss +1 -1
- package/scss/table-controls.scss +206 -0
- package/scss/themes/_dark.scss +1 -3
- package/scss/themes/_index.scss +1 -1
- package/scss/themes/_light.scss +46 -6
- package/scss/themes/_modes.scss +11 -11
- package/scss/themes/_openbyte.scss +944 -0
- package/scss/themes/_typography.scss +4 -2
- package/scss/variables/index.scss +0 -9
- package/src/components/alert/alert.component.ts +14 -4
- package/src/components/alert/alert.scss +55 -69
- package/src/components/alert/alert.test.ts +8 -0
- package/src/components/animated-button/animated-button.component.ts +3 -2
- package/src/components/animated-button/animated-button.scss +6 -23
- package/src/components/animated-button/animated-button.test.ts +4 -3
- package/src/components/avatar-group/avatar-group.component.ts +84 -0
- package/src/components/avatar-group/avatar-group.scss +54 -0
- package/src/components/avatar-group/avatar-group.test.ts +59 -0
- package/src/components/avatar-group/index.ts +12 -0
- package/src/components/badge/badge.component.ts +49 -0
- package/src/components/badge/badge.scss +80 -0
- package/src/components/badge/badge.test.ts +34 -0
- package/src/components/badge/index.ts +12 -0
- package/src/components/bulk-actions/bulk-actions.component.ts +14 -2
- package/src/components/button/button.component.ts +228 -8
- package/src/components/button/button.scss +115 -32
- package/src/components/button/button.test.ts +122 -0
- package/src/components/button-menu/button-menu.component.ts +32 -11
- package/src/components/button-menu/button-menu.scss +5 -1
- package/src/components/channel-tile/channel-tile.component.ts +34 -27
- package/src/components/channel-tile/channel-tile.scss +76 -43
- package/src/components/chart/builders.test.ts +46 -0
- package/src/components/chart/builders.ts +27 -3
- package/src/components/chart/chart-live.test.ts +4 -6
- package/src/components/chart/chart.component.ts +125 -5
- package/src/components/chart/chart.scss +121 -0
- package/src/components/chart/chart.test.ts +81 -0
- package/src/components/chat-message/chat-message.component.ts +21 -1
- package/src/components/chat-message/chat-message.scss +13 -2
- package/src/components/chat-message/chat-message.test.ts +9 -0
- package/src/components/checkbox/checkbox.component.ts +61 -27
- package/src/components/checkbox/checkbox.scss +58 -25
- package/src/components/checkbox-group/checkbox-group.component.ts +7 -1
- package/src/components/chip/chip.component.ts +11 -1
- package/src/components/chip/chip.scss +91 -26
- package/src/components/chip/chip.test.ts +17 -0
- package/src/components/collapsible/collapsible.component.ts +17 -3
- package/src/components/collapsible/collapsible.scss +19 -10
- package/src/components/color-picker/color-picker.component.ts +277 -0
- package/src/components/color-picker/color-picker.scss +141 -0
- package/src/components/color-picker/color-picker.test.ts +57 -0
- package/src/components/color-picker/index.ts +12 -0
- package/src/components/cols/cols.component.ts +10 -0
- package/src/components/cols/cols.test.ts +17 -0
- package/src/components/confirm/confirm.component.ts +20 -6
- package/src/components/confirm/confirm.test.ts +52 -0
- package/src/components/content-block/content-block.component.ts +38 -9
- package/src/components/content-block/content-block.scss +11 -5
- package/src/components/content-block/content-block.test.ts +26 -0
- package/src/components/copy-button/copy-button.component.ts +2 -2
- package/src/components/copy-button/copy-button.scss +17 -4
- package/src/components/data-select/data-select.component.ts +3 -1
- package/src/components/data-table/data-table.component.ts +362 -140
- package/src/components/data-table/data-table.scss +133 -60
- package/src/components/data-table/data-table.test.ts +356 -4
- package/src/components/data-table-filter/data-table-filter.component.ts +426 -406
- package/src/components/data-table-filter/data-table-filter.scss +176 -148
- package/src/components/data-table-filter/data-table-filter.test.ts +271 -273
- package/src/components/data-table-search/data-table-search.component.ts +4 -2
- package/src/components/data-table-search/data-table-search.scss +8 -1
- package/src/components/data-table-search/data-table-search.test.ts +29 -0
- package/src/components/date-range/date-range-math.test.ts +78 -0
- package/src/components/date-range/date-range-math.ts +105 -0
- package/src/components/date-range/date-range.component.ts +375 -0
- package/src/components/date-range/date-range.scss +272 -0
- package/src/components/date-range/date-range.test.ts +247 -0
- package/src/components/date-range/index.ts +12 -0
- package/src/components/datepicker/datepicker.component.ts +44 -7
- package/src/components/datepicker/datepicker.scss +9 -0
- package/src/components/datepicker/datepicker.test.ts +2 -2
- package/src/components/dialog/dialog.component.ts +38 -19
- package/src/components/dialog/dialog.scss +65 -36
- package/src/components/dialog/dialog.test.ts +14 -0
- package/src/components/diff/diff.component.ts +55 -0
- package/src/components/diff/diff.scss +46 -0
- package/src/components/diff/diff.test.ts +15 -0
- package/src/components/diff/index.ts +12 -0
- package/src/components/diff-row/diff-row.component.ts +75 -0
- package/src/components/diff-row/diff-row.scss +70 -0
- package/src/components/diff-row/diff-row.test.ts +37 -0
- package/src/components/diff-row/index.ts +12 -0
- package/src/components/dropdown/dropdown.component.ts +12 -10
- package/src/components/dropdown/dropdown.scss +2 -0
- package/src/components/editor/editor.component.ts +159 -11
- package/src/components/editor/editor.scss +250 -1
- package/src/components/editor/editor.test.ts +31 -3
- package/src/components/editor/modules/ai/panel/ai-panel.scss +0 -1
- package/src/components/editor/modules/attachment/attachment.ts +53 -31
- package/src/components/editor/modules/context-menu/context-menu.ts +17 -0
- package/src/components/editor/modules/dialog/dialog.component.ts +23 -1
- package/src/components/editor/modules/dialog/dialog.test.ts +28 -0
- package/src/components/editor/modules/emoji/emoji.ts +4 -5
- package/src/components/editor/modules/merge-field/merge-field.ts +83 -0
- package/src/components/editor/modules/toolbar/toolbar.component.ts +121 -68
- package/src/components/editor/modules/toolbar/toolbar.scss +38 -0
- package/src/components/editor/modules/toolbar/toolbar.ts +25 -1
- package/src/components/empty-state/empty-state.component.ts +6 -1
- package/src/components/empty-state/empty-state.scss +21 -4
- package/src/components/expanding-action/expanding-action.component.ts +22 -34
- package/src/components/expanding-action/expanding-action.scss +3 -45
- package/src/components/expanding-action/expanding-action.test.ts +2 -1
- package/src/components/file/file.component.ts +30 -2
- package/src/components/file/file.scss +6 -0
- package/src/components/flow-builder/flow-builder.component.ts +36 -3
- package/src/components/flow-builder/flow-builder.scss +0 -6
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +8 -0
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.scss +10 -5
- package/src/components/flow-builder/modules/flow-node/flow-node.component.ts +17 -0
- package/src/components/flow-builder/modules/flow-node/flow-node.scss +10 -0
- package/src/components/form-actions/form-actions.component.ts +48 -2
- package/src/components/form-actions/form-actions.scss +1 -1
- package/src/components/form-group/form-group.component.ts +10 -3
- package/src/components/form-group/form-group.scss +37 -4
- package/src/components/form-group/form-group.test.ts +10 -0
- package/src/components/header/header.component.ts +11 -4
- package/src/components/header/header.scss +47 -5
- package/src/components/hover-container/hover-container.component.ts +4 -5
- package/src/components/icon/icon.component.ts +95 -9
- package/src/components/icon/icon.scss +70 -6
- package/src/components/icon/icon.test.ts +55 -0
- package/src/components/icon-picker/icon-picker.component.ts +35 -1
- package/src/components/inline-edit/inline-edit.component.ts +38 -8
- package/src/components/inline-edit/inline-edit.scss +128 -19
- package/src/components/input/input.component.ts +173 -297
- package/src/components/input/input.scss +65 -43
- package/src/components/input/input.test.ts +86 -4
- package/src/components/item/item.component.ts +12 -0
- package/src/components/item/item.scss +102 -3
- package/src/components/item/item.test.ts +23 -0
- package/src/components/key/key.component.ts +2 -2
- package/src/components/key/key.scss +11 -0
- package/src/components/key-value/index.ts +12 -0
- package/src/components/key-value/key-value.component.ts +160 -0
- package/src/components/key-value/key-value.scss +20 -0
- package/src/components/key-value/key-value.test.ts +22 -0
- package/src/components/markdown-editor/markdown-editor.component.ts +8 -1
- package/src/components/menu/menu.component.ts +21 -2
- package/src/components/menu/menu.scss +43 -23
- package/src/components/menu/menu.test.ts +33 -0
- package/src/components/menu-item/menu-item.component.ts +13 -2
- package/src/components/menu-item/menu-item.scss +56 -23
- package/src/components/menu-item/menu-item.test.ts +31 -0
- package/src/components/menu-item/submenu-controller.ts +25 -15
- package/src/components/menu-label/index.ts +12 -0
- package/src/components/menu-label/menu-label.component.ts +30 -0
- package/src/components/menu-label/menu-label.scss +18 -0
- package/src/components/nav-item/index.ts +12 -0
- package/src/components/nav-item/nav-item.component.ts +83 -0
- package/src/components/nav-item/nav-item.scss +128 -0
- package/src/components/nav-list/index.ts +12 -0
- package/src/components/nav-list/nav-list.component.ts +313 -0
- package/src/components/nav-list/nav-list.scss +87 -0
- package/src/components/nav-list/nav-list.test.ts +250 -0
- package/src/components/navbar/navbar.component.ts +235 -171
- package/src/components/navbar/navbar.scss +333 -169
- package/src/components/navbar/navbar.test.ts +63 -143
- package/src/components/page/page.component.ts +114 -29
- package/src/components/page/page.scss +223 -95
- package/src/components/page/page.test.ts +214 -3
- package/src/components/page-builder/page-builder.component.ts +51 -5
- package/src/components/page-builder/page-builder.scss +6 -1
- package/src/components/page-nav/page-nav.component.ts +25 -3
- package/src/components/page-nav/page-nav.scss +10 -2
- package/src/components/pagination/pagination.scss +1 -1
- package/src/components/panel/panel.component.ts +14 -13
- package/src/components/panel/panel.scss +14 -10
- package/src/components/panel/panel.test.ts +42 -0
- package/src/components/popup/popup.component.ts +20 -13
- package/src/components/popup/popup.test.ts +21 -1
- package/src/components/priority-list/priority-list.component.ts +157 -172
- package/src/components/priority-list/priority-list.scss +113 -17
- package/src/components/priority-list/priority-list.test.ts +301 -3
- package/src/components/progress-bar/progress-bar.component.ts +1 -1
- package/src/components/progress-bar/progress-bar.scss +16 -4
- package/src/components/progress-tile/progress-tile.scss +1 -1
- package/src/components/query-builder/query-builder.component.ts +85 -10
- package/src/components/query-builder/query-builder.scss +7 -1
- package/src/components/query-builder/query-builder.test.ts +40 -0
- package/src/components/radio/radio.component.ts +57 -18
- package/src/components/radio/radio.scss +101 -26
- package/src/components/radio-group/radio-group.component.ts +45 -7
- package/src/components/radio-group/radio-group.scss +17 -0
- package/src/components/radio-group/radio-group.test.ts +63 -2
- package/src/components/rating/rating.component.ts +23 -0
- package/src/components/rating/rating.scss +1 -1
- package/src/components/remarkd-editor/remarkd-editor.component.ts +60 -5
- package/src/components/remarkd-editor/remarkd-editor.scss +12 -4
- package/src/components/report-table/index.ts +12 -0
- package/src/components/report-table/report-format.test.ts +86 -0
- package/src/components/report-table/report-format.ts +72 -0
- package/src/components/report-table/report-math.test.ts +74 -0
- package/src/components/report-table/report-math.ts +252 -0
- package/src/components/report-table/report-table.component.ts +681 -0
- package/src/components/report-table/report-table.scss +328 -0
- package/src/components/report-table/report-table.test.ts +702 -0
- package/src/components/reveal/reveal.component.ts +15 -1
- package/src/components/reveal/reveal.scss +5 -0
- package/src/components/schedule-builder/schedule-builder.component.ts +23 -8
- package/src/components/scroll-container/scroll-container.component.ts +19 -1
- package/src/components/scroll-container/scroll-container.scss +6 -1
- package/src/components/select/select.component.ts +52 -36
- package/src/components/select/select.scss +42 -25
- package/src/components/select/select.test.ts +14 -2
- package/src/components/settings-container/settings-container.scss +18 -5
- package/src/components/sidebar/sidebar.component.ts +8 -1
- package/src/components/sidebar/sidebar.scss +6 -1
- package/src/components/skeleton/skeleton.component.ts +3 -3
- package/src/components/skeleton/skeleton.scss +6 -6
- package/src/components/skeleton/skeleton.test.ts +9 -0
- package/src/components/slash-menu/slash-menu.scss +1 -1
- package/src/components/slideout/slideout.component.ts +20 -3
- package/src/components/slideout/slideout.scss +13 -6
- package/src/components/slideout/slideout.test.ts +19 -0
- package/src/components/split-button/split-button.component.ts +1 -1
- package/src/components/split-pane/split-pane.component.ts +71 -8
- package/src/components/split-pane/split-pane.scss +145 -41
- package/src/components/split-pane/split-pane.test.ts +23 -3
- package/src/components/stat/stat.component.ts +209 -14
- package/src/components/stat/stat.scss +305 -5
- package/src/components/stat/stat.test.ts +143 -2
- package/src/components/status-indicator/status-indicator.scss +22 -21
- package/src/components/stepper/stepper.scss +3 -3
- package/src/components/tab/tab.component.ts +1 -0
- package/src/components/table/table.component.ts +12 -1
- package/src/components/table/table.scss +21 -13
- package/src/components/table/table.test.ts +17 -0
- package/src/components/tabs/tabs.component.ts +150 -37
- package/src/components/tabs/tabs.scss +48 -7
- package/src/components/tabs/tabs.test.ts +169 -0
- package/src/components/textarea/textarea.component.ts +25 -3
- package/src/components/textarea/textarea.scss +10 -0
- package/src/components/textarea/textarea.test.ts +8 -4
- package/src/components/theme-editor/theme-editor.scss +0 -6
- package/src/components/tile/tile.component.ts +32 -2
- package/src/components/tile/tile.scss +40 -7
- package/src/components/tile/tile.test.ts +10 -0
- package/src/components/toggle/toggle.component.ts +23 -7
- package/src/components/toggle/toggle.scss +36 -29
- package/src/components/toggle/toggle.test.ts +17 -0
- package/src/components/tooltip/tooltip.component.ts +3 -0
- package/src/components/tooltip/tooltip.scss +11 -6
- package/src/components/tooltip/tooltip.test.ts +12 -0
- package/src/components/translation-field/index.ts +12 -0
- package/src/components/translation-field/translation-field.component.ts +55 -0
- package/src/components/translation-field/translation-field.scss +101 -0
- package/src/components/translation-fields/index.ts +12 -0
- package/src/components/translation-fields/translation-fields.component.ts +80 -0
- package/src/components/translation-fields/translation-fields.scss +60 -0
- package/src/components/translation-grid/index.ts +12 -0
- package/src/components/translation-grid/translation-grid.component.ts +157 -0
- package/src/components/translation-grid/translation-grid.scss +138 -0
- package/src/components/translation-grid/translation-grid.test.ts +28 -0
- package/src/components/vertical-stepper/vertical-stepper.component.ts +35 -9
- package/src/components/vertical-stepper/vertical-stepper.scss +110 -39
- package/src/components/vertical-stepper/vertical-stepper.test.ts +35 -0
- package/src/components/well/well.scss +2 -2
- package/src/events/events.ts +2 -0
- package/src/events/zn-chart-click.ts +13 -0
- package/src/events/zn-report-change.ts +7 -0
- package/src/form-control.scss +36 -3
- package/src/internal/anchored-popover.ts +175 -0
- package/src/internal/animate.ts +19 -5
- package/src/internal/cosmic-border.ts +15 -0
- package/src/internal/form.ts +1 -1
- package/src/internal/hover-intent.ts +15 -0
- package/src/internal/input-modality.ts +15 -0
- package/src/internal/pointer-intent.ts +37 -0
- package/src/internal/reorder.ts +625 -0
- package/src/internal/scroll.ts +51 -3
- package/src/internal/selection-card.ts +1 -1
- package/src/internal/tab-indicator.ts +92 -0
- package/src/internal/table-sort.ts +25 -0
- package/src/internal/table-tabs.ts +108 -0
- package/src/internal/test-drag.ts +35 -0
- package/src/internal/theme-openbyte.test.ts +126 -0
- package/src/internal/theme.ts +4 -4
- package/src/internal/zinc-element.ts +2 -2
- package/src/selection-card.scss +25 -0
- package/src/types/web-test-runner-commands.d.ts +1 -0
- package/src/utilities/color.test.ts +43 -0
- package/src/utilities/color.ts +189 -0
- package/src/zinc.ts +14 -0
- package/.github/workflows/eleventy_build.yml +0 -42
- package/scss/themes/_aura.scss +0 -114
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import {EASE_OUT, prefersReducedMotion} from './animate';
|
|
2
|
+
import type {FormControlController} from './form';
|
|
3
|
+
import type {ReactiveController, ReactiveControllerHost} from 'lit';
|
|
4
|
+
|
|
5
|
+
/** A component whose keyed items can be dragged, or moved by keyboard, into a new order. */
|
|
6
|
+
export interface ReorderHost extends ReactiveControllerHost, HTMLElement {
|
|
7
|
+
/** The item keys in their current order. */
|
|
8
|
+
reorderKeys: () => string[];
|
|
9
|
+
|
|
10
|
+
/** Shows the items in a new order. */
|
|
11
|
+
applyReorder: (keys: string[]) => void;
|
|
12
|
+
|
|
13
|
+
/** The row drawn for an item, measured to slide it from its old position. */
|
|
14
|
+
reorderRow: (key: string) => HTMLElement | null;
|
|
15
|
+
|
|
16
|
+
/** Every element that slides with an item, when more than its row does (a table column's cells). */
|
|
17
|
+
reorderSlides?: (key: string) => HTMLElement[];
|
|
18
|
+
|
|
19
|
+
/** Whether an item is pinned where it is; nothing can move past it either. */
|
|
20
|
+
reorderLocked?: (key: string) => boolean;
|
|
21
|
+
|
|
22
|
+
/** Where the pointer must be for a drag to land; letting go outside it cancels. Defaults to the rows' parent. */
|
|
23
|
+
reorderArea?: () => HTMLElement | null;
|
|
24
|
+
|
|
25
|
+
/** The element that follows the pointer while dragging. Defaults to a copy of the item's row. */
|
|
26
|
+
reorderGhost?: (key: string) => HTMLElement;
|
|
27
|
+
|
|
28
|
+
/** Submits the form after each reorder when set: to this URL, or the form's own action when empty. */
|
|
29
|
+
formAction?: string;
|
|
30
|
+
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ReorderOptions {
|
|
35
|
+
/** Submits through the host's form, which announces the form so a host page can intercept the submit. */
|
|
36
|
+
formControl?: FormControlController;
|
|
37
|
+
|
|
38
|
+
/** Fields for a temporary form when the host has a `formaction` but no form of its own. */
|
|
39
|
+
fallbackFields?: () => [string, string][];
|
|
40
|
+
|
|
41
|
+
/** Keys that move the focused item. With `alt`, plain arrows are left to navigate between items. */
|
|
42
|
+
modifier?: 'none' | 'alt';
|
|
43
|
+
|
|
44
|
+
/** Focuses an item after it moves by keyboard. */
|
|
45
|
+
focus?: (key: string) => void;
|
|
46
|
+
|
|
47
|
+
/** Which way the items run: arrow keys, the drag midpoint and the slide follow it. */
|
|
48
|
+
axis?: 'vertical' | 'horizontal';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Leaves the items in place while dragging and tracks `dropIndex` for the host to draw a drop line, moving the item
|
|
52
|
+
* only on drop. Suits items of very different sizes, which would jump under the pointer when moved live.
|
|
53
|
+
*/
|
|
54
|
+
indicator?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type ReorderEvent = 'zn-change' | 'zn-reorder';
|
|
58
|
+
|
|
59
|
+
interface Point {
|
|
60
|
+
x: number;
|
|
61
|
+
y: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// How far a mouse or pen moves before a press becomes a drag, so a click still clicks
|
|
65
|
+
const DRAG_THRESHOLD = 4;
|
|
66
|
+
// How long a touch holds still before it drags; moving sooner scrolls instead
|
|
67
|
+
const TOUCH_HOLD = 300;
|
|
68
|
+
// The band at a scroller's edge that scrolls it while dragging
|
|
69
|
+
const SCROLL_EDGE = 40;
|
|
70
|
+
const SCROLL_SPEED = 12;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Pointer and keyboard reordering for a keyed list, with the change announced through `zn-change` and `zn-reorder`
|
|
74
|
+
* and, when the host has a `formaction`, submitted with its form.
|
|
75
|
+
*
|
|
76
|
+
* Dragging runs on pointer events rather than native drag and drop, which hands the cursor to the browser for the
|
|
77
|
+
* whole drag.
|
|
78
|
+
*/
|
|
79
|
+
export class ReorderController implements ReactiveController {
|
|
80
|
+
/** The item being dragged, styled as its placeholder. */
|
|
81
|
+
draggedKey: string | null = null;
|
|
82
|
+
|
|
83
|
+
/** Whether a drag is under way, for transitions while items shift. */
|
|
84
|
+
isDragging = false;
|
|
85
|
+
|
|
86
|
+
/** With `indicator`, the boundary the dragged item would drop at: before the item at this index, or after the last. */
|
|
87
|
+
dropIndex: number | null = null;
|
|
88
|
+
|
|
89
|
+
private readonly host: ReorderHost;
|
|
90
|
+
private readonly options: ReorderOptions;
|
|
91
|
+
private dragKey: string | null = null;
|
|
92
|
+
private startOrder: string[] = [];
|
|
93
|
+
private sliding = 0;
|
|
94
|
+
private listOverflow = '';
|
|
95
|
+
private press: {key: string; pointerId: number; touch: boolean; origin: Point; hold?: number} | null = null;
|
|
96
|
+
private point: Point = {x: 0, y: 0};
|
|
97
|
+
private ghost: HTMLElement | null = null;
|
|
98
|
+
private ghostOffset: Point = {x: 0, y: 0};
|
|
99
|
+
private shield: HTMLElement | null = null;
|
|
100
|
+
private scroller: Element | null = null;
|
|
101
|
+
private scrollFrame = 0;
|
|
102
|
+
|
|
103
|
+
constructor(host: ReorderHost, options: ReorderOptions = {}) {
|
|
104
|
+
this.host = host;
|
|
105
|
+
this.options = options;
|
|
106
|
+
host.addController(this);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
hostConnected() {
|
|
110
|
+
// Nothing to set up; the host wires pointerdown to its own rows.
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
hostDisconnected() {
|
|
114
|
+
if (this.isDragging) this.endDrag(true);
|
|
115
|
+
this.release();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private get horizontal() {
|
|
119
|
+
return this.options.axis === 'horizontal';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The leading edge of a row along the axis. */
|
|
123
|
+
private start(row: HTMLElement | null) {
|
|
124
|
+
const rect = row?.getBoundingClientRect();
|
|
125
|
+
return this.horizontal ? rect?.left : rect?.top;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private keys() {
|
|
129
|
+
return this.host.reorderKeys();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
isLocked(key: string) {
|
|
133
|
+
return this.host.reorderLocked?.(key) ?? false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A move is allowed when neither the moved item nor any item it would shift past is locked. */
|
|
137
|
+
canMove(fromIndex: number, toIndex: number) {
|
|
138
|
+
const keys = this.keys();
|
|
139
|
+
if (fromIndex < 0 || toIndex < 0 || fromIndex >= keys.length || toIndex >= keys.length) return false;
|
|
140
|
+
if (this.isLocked(keys[fromIndex])) return false;
|
|
141
|
+
const [low, high] = fromIndex < toIndex ? [fromIndex + 1, toIndex] : [toIndex, fromIndex - 1];
|
|
142
|
+
for (let i = low; i <= high; i++) {
|
|
143
|
+
if (this.isLocked(keys[i])) return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
canStep(key: string, step: number) {
|
|
149
|
+
const index = this.keys().indexOf(key);
|
|
150
|
+
return index !== -1 && this.canMove(index, index + step);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Moves an item and announces the new order. */
|
|
154
|
+
move(fromIndex: number, toIndex: number) {
|
|
155
|
+
if (fromIndex === toIndex || !this.canMove(fromIndex, toIndex)) return false;
|
|
156
|
+
this.reorder(moved(this.keys(), fromIndex, toIndex));
|
|
157
|
+
this.commit();
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
step(key: string, step: number) {
|
|
162
|
+
const index = this.keys().indexOf(key);
|
|
163
|
+
return this.move(index, index + step);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Arrow keys move the item one place and Home/End to either end. Returns whether the key was handled. */
|
|
167
|
+
handleKeyDown(event: KeyboardEvent, key: string) {
|
|
168
|
+
if (this.host.disabled) return false;
|
|
169
|
+
if ((this.options.modifier ?? 'none') === 'alt' && !event.altKey) return false;
|
|
170
|
+
|
|
171
|
+
const keys = this.keys();
|
|
172
|
+
const index = keys.indexOf(key);
|
|
173
|
+
if (index === -1) return false;
|
|
174
|
+
|
|
175
|
+
const [back, forward] = this.horizontal ? ['ArrowLeft', 'ArrowRight'] : ['ArrowUp', 'ArrowDown'];
|
|
176
|
+
const target = {
|
|
177
|
+
[back]: index - 1,
|
|
178
|
+
[forward]: index + 1,
|
|
179
|
+
Home: 0,
|
|
180
|
+
End: keys.length - 1,
|
|
181
|
+
}[event.key];
|
|
182
|
+
if (target === undefined || target === index || target < 0 || target >= keys.length) return false;
|
|
183
|
+
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
if (this.move(index, target)) {
|
|
186
|
+
void this.host.updateComplete.then(() => this.options.focus?.(key));
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Starts tracking a press on an item; it becomes a drag once the pointer moves, or a touch holds still. */
|
|
192
|
+
handlePointerDown(event: PointerEvent, key: string) {
|
|
193
|
+
if (event.button !== 0 || this.press || this.isDragging) return;
|
|
194
|
+
if (this.host.disabled || this.isLocked(key) || !this.host.reorderRow(key)) return;
|
|
195
|
+
if (event.composedPath().some(el => el instanceof HTMLElement && el.matches('input, textarea, select, [contenteditable]'))) return;
|
|
196
|
+
|
|
197
|
+
const touch = event.pointerType === 'touch';
|
|
198
|
+
this.point = {x: event.clientX, y: event.clientY};
|
|
199
|
+
this.press = {key, pointerId: event.pointerId, touch, origin: this.point};
|
|
200
|
+
if (touch) this.press.hold = window.setTimeout(() => this.beginDrag(), TOUCH_HOLD);
|
|
201
|
+
|
|
202
|
+
window.addEventListener('pointermove', this.onPointerMove, true);
|
|
203
|
+
window.addEventListener('pointerup', this.onPointerUp, true);
|
|
204
|
+
window.addEventListener('pointercancel', this.onPointerCancel, true);
|
|
205
|
+
window.addEventListener('touchmove', this.onTouchMove, {capture: true, passive: false});
|
|
206
|
+
// A link or image in the row would otherwise start a native drag and take the pointer
|
|
207
|
+
window.addEventListener('dragstart', this.onNativeDragStart, true);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private onPointerMove = (event: PointerEvent) => {
|
|
211
|
+
if (event.pointerId !== this.press?.pointerId) return;
|
|
212
|
+
this.point = {x: event.clientX, y: event.clientY};
|
|
213
|
+
|
|
214
|
+
if (!this.isDragging) {
|
|
215
|
+
const {origin, touch} = this.press;
|
|
216
|
+
if (Math.hypot(this.point.x - origin.x, this.point.y - origin.y) < DRAG_THRESHOLD) return;
|
|
217
|
+
// A touch that moves before the hold is a scroll
|
|
218
|
+
if (touch) this.release();
|
|
219
|
+
else this.beginDrag();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.placeGhost();
|
|
224
|
+
this.dragOver();
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
private onPointerUp = (event: PointerEvent) => {
|
|
228
|
+
if (event.pointerId !== this.press?.pointerId) return;
|
|
229
|
+
this.point = {x: event.clientX, y: event.clientY};
|
|
230
|
+
if (this.isDragging) {
|
|
231
|
+
this.endDrag(!this.inArea());
|
|
232
|
+
// The release still clicks, which would sort a column or select an item
|
|
233
|
+
window.addEventListener('click', swallow, {capture: true, once: true});
|
|
234
|
+
setTimeout(() => window.removeEventListener('click', swallow, true));
|
|
235
|
+
}
|
|
236
|
+
this.release();
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
private onPointerCancel = (event: PointerEvent) => {
|
|
240
|
+
if (event.pointerId !== this.press?.pointerId) return;
|
|
241
|
+
if (this.isDragging) this.endDrag(true);
|
|
242
|
+
this.release();
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
private onTouchMove = (event: TouchEvent) => {
|
|
246
|
+
if (this.isDragging) event.preventDefault();
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
private onNativeDragStart = (event: DragEvent) => {
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
private onKeyDown = (event: KeyboardEvent) => {
|
|
254
|
+
if (event.key !== 'Escape') return;
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
event.stopPropagation();
|
|
257
|
+
this.endDrag(true);
|
|
258
|
+
this.release();
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
private beginDrag() {
|
|
262
|
+
const key = this.press?.key;
|
|
263
|
+
const row = key ? this.host.reorderRow(key) : null;
|
|
264
|
+
if (!key || !row) {
|
|
265
|
+
this.release();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
window.clearTimeout(this.press!.hold);
|
|
269
|
+
|
|
270
|
+
const rect = row.getBoundingClientRect();
|
|
271
|
+
const custom = this.host.reorderGhost?.(key);
|
|
272
|
+
const ghost = custom ?? snapshot(row);
|
|
273
|
+
this.ghostOffset = custom ? {x: 12, y: 16} : {x: this.point.x - rect.left, y: this.point.y - rect.top};
|
|
274
|
+
Object.assign(ghost.style, {
|
|
275
|
+
position: 'fixed',
|
|
276
|
+
left: '0',
|
|
277
|
+
top: '0',
|
|
278
|
+
margin: '0',
|
|
279
|
+
zIndex: '2147483647',
|
|
280
|
+
pointerEvents: 'none',
|
|
281
|
+
});
|
|
282
|
+
if (!custom) {
|
|
283
|
+
Object.assign(ghost.style, {
|
|
284
|
+
width: `${rect.width}px`,
|
|
285
|
+
height: `${rect.height}px`,
|
|
286
|
+
boxSizing: 'border-box',
|
|
287
|
+
background: 'rgb(var(--zn-panel, 255, 255, 255))',
|
|
288
|
+
boxShadow: 'var(--zn-shadow-large, 0 8px 24px rgba(13, 23, 56, 0.16))',
|
|
289
|
+
borderRadius: getComputedStyle(row).borderRadius,
|
|
290
|
+
opacity: '0.95',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
ghost.inert = true;
|
|
294
|
+
|
|
295
|
+
// Covers the page while dragging, so the cursor stays a closed hand over everything and nothing else reacts
|
|
296
|
+
const shield = document.createElement('div');
|
|
297
|
+
Object.assign(shield.style, {position: 'fixed', inset: '0', zIndex: '2147483646', cursor: 'grabbing', touchAction: 'none'});
|
|
298
|
+
|
|
299
|
+
// Inside the row's own tree so its styles reach the copy, and a modal dialog around it doesn't make it inert
|
|
300
|
+
const root = row.getRootNode();
|
|
301
|
+
const container = root instanceof ShadowRoot ? root : document.body;
|
|
302
|
+
container.append(shield, ghost);
|
|
303
|
+
this.ghost = ghost;
|
|
304
|
+
this.shield = shield;
|
|
305
|
+
this.placeGhost();
|
|
306
|
+
|
|
307
|
+
getSelection()?.removeAllRanges();
|
|
308
|
+
this.dragKey = key;
|
|
309
|
+
this.draggedKey = key;
|
|
310
|
+
this.startOrder = this.keys();
|
|
311
|
+
this.isDragging = true;
|
|
312
|
+
this.scroller = scrollerOf(this.area(), this.horizontal);
|
|
313
|
+
this.scrollFrame = requestAnimationFrame(this.autoScroll);
|
|
314
|
+
window.addEventListener('keydown', this.onKeyDown, true);
|
|
315
|
+
this.host.requestUpdate();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private placeGhost() {
|
|
319
|
+
if (!this.ghost) return;
|
|
320
|
+
this.ghost.style.transform = `translate(${this.point.x - this.ghostOffset.x}px, ${this.point.y - this.ghostOffset.y}px)`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private area() {
|
|
324
|
+
return this.host.reorderArea?.() ?? this.host.reorderRow(this.keys()[0])?.parentElement ?? null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private inArea() {
|
|
328
|
+
const rect = this.area()?.getBoundingClientRect();
|
|
329
|
+
return !!rect && this.point.x >= rect.left && this.point.x <= rect.right && this.point.y >= rect.top && this.point.y <= rect.bottom;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// The item whose span along the axis holds the pointer; none in a gap between items or outside the list
|
|
333
|
+
private hoveredKey() {
|
|
334
|
+
if (!this.inArea()) return null;
|
|
335
|
+
const at = this.horizontal ? this.point.x : this.point.y;
|
|
336
|
+
return this.keys().find(key => {
|
|
337
|
+
const rect = this.host.reorderRow(key)?.getBoundingClientRect();
|
|
338
|
+
if (!rect) return false;
|
|
339
|
+
return this.horizontal ? at >= rect.left && at < rect.right : at >= rect.top && at < rect.bottom;
|
|
340
|
+
}) ?? null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Moves the dragged item to the hovered one's place, once the pointer crosses that item's midpoint. In a gap
|
|
345
|
+
* between items nothing moves, and letting go there keeps the order so far.
|
|
346
|
+
*/
|
|
347
|
+
private dragOver() {
|
|
348
|
+
const key = this.hoveredKey();
|
|
349
|
+
if (this.options.indicator) {
|
|
350
|
+
this.trackDropIndex(key);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (!key || !this.dragKey || this.dragKey === key) return;
|
|
354
|
+
|
|
355
|
+
const keys = this.keys();
|
|
356
|
+
const fromIndex = keys.indexOf(this.dragKey);
|
|
357
|
+
const toIndex = keys.indexOf(key);
|
|
358
|
+
if (fromIndex === -1 || toIndex === -1) return;
|
|
359
|
+
|
|
360
|
+
// Rows of different sizes would otherwise flip back and forth
|
|
361
|
+
if (fromIndex < toIndex !== this.pastMidpoint(key)) return;
|
|
362
|
+
if (!this.canMove(fromIndex, toIndex)) return;
|
|
363
|
+
|
|
364
|
+
this.reorder(moved(keys, fromIndex, toIndex));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private pastMidpoint(key: string) {
|
|
368
|
+
const rect = this.host.reorderRow(key)!.getBoundingClientRect();
|
|
369
|
+
return this.horizontal
|
|
370
|
+
? this.point.x > rect.left + rect.width / 2
|
|
371
|
+
: this.point.y > rect.top + rect.height / 2;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// The nearest boundary of the hovered item; a gap between items keeps the last one, and leaving the list clears it
|
|
375
|
+
private trackDropIndex(key: string | null) {
|
|
376
|
+
if (!this.dragKey) return;
|
|
377
|
+
if (!key) {
|
|
378
|
+
if (this.inArea()) return;
|
|
379
|
+
this.setDropIndex(null);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const keys = this.keys();
|
|
384
|
+
const fromIndex = keys.indexOf(this.dragKey);
|
|
385
|
+
const index = keys.indexOf(key);
|
|
386
|
+
if (fromIndex === -1 || index === -1) return;
|
|
387
|
+
|
|
388
|
+
const boundary = index + (this.pastMidpoint(key) ? 1 : 0);
|
|
389
|
+
const toIndex = this.dropTarget(fromIndex, boundary);
|
|
390
|
+
this.setDropIndex(toIndex !== fromIndex && this.canMove(fromIndex, toIndex) ? boundary : null);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private setDropIndex(dropIndex: number | null) {
|
|
394
|
+
if (dropIndex === this.dropIndex) return;
|
|
395
|
+
this.dropIndex = dropIndex;
|
|
396
|
+
this.host.requestUpdate();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// The index the item moves to once taken out from before the boundary
|
|
400
|
+
private dropTarget(fromIndex: number, boundary: number) {
|
|
401
|
+
return boundary > fromIndex ? boundary - 1 : boundary;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Scrolls the list's scroller while the pointer sits in the band at either end of it
|
|
405
|
+
private autoScroll = () => {
|
|
406
|
+
if (!this.isDragging) return;
|
|
407
|
+
this.scrollFrame = requestAnimationFrame(this.autoScroll);
|
|
408
|
+
|
|
409
|
+
const scroller = this.scroller;
|
|
410
|
+
if (!scroller) return;
|
|
411
|
+
const page = scroller === document.scrollingElement;
|
|
412
|
+
const rect = page
|
|
413
|
+
? {left: 0, top: 0, right: innerWidth, bottom: innerHeight}
|
|
414
|
+
: scroller.getBoundingClientRect();
|
|
415
|
+
const [at, start, end] = this.horizontal
|
|
416
|
+
? [this.point.x, rect.left, rect.right]
|
|
417
|
+
: [this.point.y, rect.top, rect.bottom];
|
|
418
|
+
|
|
419
|
+
const into = at < start + SCROLL_EDGE ? at - (start + SCROLL_EDGE) : at > end - SCROLL_EDGE ? at - (end - SCROLL_EDGE) : 0;
|
|
420
|
+
if (!into) return;
|
|
421
|
+
const by = Math.max(-1, Math.min(1, into / SCROLL_EDGE)) * SCROLL_SPEED;
|
|
422
|
+
const before = this.horizontal ? scroller.scrollLeft : scroller.scrollTop;
|
|
423
|
+
scroller.scrollBy(this.horizontal ? {left: by} : {top: by});
|
|
424
|
+
if ((this.horizontal ? scroller.scrollLeft : scroller.scrollTop) !== before) this.dragOver();
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
private endDrag(cancel: boolean) {
|
|
428
|
+
if (!this.isDragging) return;
|
|
429
|
+
|
|
430
|
+
if (!cancel && this.options.indicator && this.dropIndex !== null && this.dragKey) {
|
|
431
|
+
const fromIndex = this.keys().indexOf(this.dragKey);
|
|
432
|
+
this.reorder(moved(this.keys(), fromIndex, this.dropTarget(fromIndex, this.dropIndex)));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const startOrder = this.startOrder;
|
|
436
|
+
this.dragKey = null;
|
|
437
|
+
this.draggedKey = null;
|
|
438
|
+
this.dropIndex = null;
|
|
439
|
+
this.isDragging = false;
|
|
440
|
+
cancelAnimationFrame(this.scrollFrame);
|
|
441
|
+
window.removeEventListener('keydown', this.onKeyDown, true);
|
|
442
|
+
this.ghost?.remove();
|
|
443
|
+
this.shield?.remove();
|
|
444
|
+
this.ghost = null;
|
|
445
|
+
this.shield = null;
|
|
446
|
+
this.scroller = null;
|
|
447
|
+
this.host.requestUpdate();
|
|
448
|
+
|
|
449
|
+
const changed = startOrder.join('\n') !== this.keys().join('\n');
|
|
450
|
+
if (!changed) return;
|
|
451
|
+
if (cancel) {
|
|
452
|
+
this.reorder(startOrder);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
this.commit();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Stops tracking the press, whether or not it became a drag
|
|
459
|
+
private release() {
|
|
460
|
+
if (this.press) window.clearTimeout(this.press.hold);
|
|
461
|
+
this.press = null;
|
|
462
|
+
window.removeEventListener('pointermove', this.onPointerMove, true);
|
|
463
|
+
window.removeEventListener('pointerup', this.onPointerUp, true);
|
|
464
|
+
window.removeEventListener('pointercancel', this.onPointerCancel, true);
|
|
465
|
+
window.removeEventListener('touchmove', this.onTouchMove, true);
|
|
466
|
+
window.removeEventListener('dragstart', this.onNativeDragStart, true);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Shows the new order, sliding each row from its old position. */
|
|
470
|
+
reorder(keys: string[]) {
|
|
471
|
+
const before = new Map(this.keys().map(key => [key, this.start(this.host.reorderRow(key))]));
|
|
472
|
+
this.host.applyReorder(keys);
|
|
473
|
+
|
|
474
|
+
if (prefersReducedMotion()) return;
|
|
475
|
+
void this.host.updateComplete.then(() => {
|
|
476
|
+
const slides: Animation[] = [];
|
|
477
|
+
for (const key of keys) {
|
|
478
|
+
const row = this.host.reorderRow(key);
|
|
479
|
+
const start = before.get(key);
|
|
480
|
+
if (!row || start === undefined) continue;
|
|
481
|
+
const delta = start - this.start(row)!;
|
|
482
|
+
if (!delta) continue;
|
|
483
|
+
const from = this.horizontal ? `translateX(${delta}px)` : `translateY(${delta}px)`;
|
|
484
|
+
for (const el of this.host.reorderSlides?.(key) ?? [row]) {
|
|
485
|
+
slides.push(el.animate([{transform: from}, {transform: 'none'}], {duration: 150, easing: EASE_OUT}));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
this.clipWhileSliding(this.host.reorderRow(keys[0])?.parentElement, slides);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// A row can change size with its place (a flush list's last row has no border), so it pokes out of the list
|
|
493
|
+
// mid-slide and flashes a scrollbar on the scroller around it
|
|
494
|
+
private clipWhileSliding(list: HTMLElement | null | undefined, slides: Animation[]) {
|
|
495
|
+
if (!list || !slides.length) return;
|
|
496
|
+
if (this.sliding++ === 0) {
|
|
497
|
+
this.listOverflow = list.style.overflow;
|
|
498
|
+
list.style.overflow = 'clip';
|
|
499
|
+
}
|
|
500
|
+
void Promise.allSettled(slides.map(slide => slide.finished)).then(() => {
|
|
501
|
+
if (--this.sliding === 0) list.style.overflow = this.listOverflow;
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private commit() {
|
|
506
|
+
this.emit('zn-change');
|
|
507
|
+
this.emit('zn-reorder');
|
|
508
|
+
this.submit();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private emit(name: ReorderEvent) {
|
|
512
|
+
this.host.dispatchEvent(new CustomEvent(name, {bubbles: true, composed: true}));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Submits the host's form when it has a `formaction`. */
|
|
516
|
+
private submit() {
|
|
517
|
+
const formAction = this.host.formAction;
|
|
518
|
+
if (formAction === undefined || formAction === null) return;
|
|
519
|
+
|
|
520
|
+
const formControl = this.options.formControl;
|
|
521
|
+
if (formControl?.getForm()) {
|
|
522
|
+
// Submitting through the controller announces the form before it is submitted, which is what lets a host
|
|
523
|
+
// page intercept the submit: a submit event raised on a form inside a shadow root never reaches the document.
|
|
524
|
+
const submitter = document.createElement('input');
|
|
525
|
+
submitter.type = 'submit';
|
|
526
|
+
if (formAction) {
|
|
527
|
+
submitter.setAttribute('formaction', formAction);
|
|
528
|
+
}
|
|
529
|
+
formControl.submit(submitter);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (!formAction) return;
|
|
534
|
+
const form = document.createElement('form');
|
|
535
|
+
form.method = 'post';
|
|
536
|
+
form.action = formAction;
|
|
537
|
+
form.hidden = true;
|
|
538
|
+
for (const [name, value] of this.options.fallbackFields?.() ?? []) {
|
|
539
|
+
const input = document.createElement('input');
|
|
540
|
+
input.type = 'hidden';
|
|
541
|
+
input.name = name;
|
|
542
|
+
input.value = value;
|
|
543
|
+
form.append(input);
|
|
544
|
+
}
|
|
545
|
+
document.body.append(form);
|
|
546
|
+
// requestSubmit raises a cancellable submit event, so a host page can handle it rather than navigating
|
|
547
|
+
form.requestSubmit();
|
|
548
|
+
form.remove();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function swallow(event: Event) {
|
|
553
|
+
event.preventDefault();
|
|
554
|
+
event.stopPropagation();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** A copy of a row to drag, with each slot replaced by what it shows, as a copy outside the slot's host shows nothing. */
|
|
558
|
+
function snapshot(row: HTMLElement) {
|
|
559
|
+
const copy = row.cloneNode(true) as HTMLElement;
|
|
560
|
+
const slots = row.querySelectorAll('slot');
|
|
561
|
+
const copies = copy.querySelectorAll('slot');
|
|
562
|
+
slots.forEach((slot, i) => copies[i].replaceWith(...slot.assignedNodes({flatten: true}).map(node => node.cloneNode(true))));
|
|
563
|
+
copy.removeAttribute('id');
|
|
564
|
+
return copy;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** The nearest element that scrolls along the axis, crossing shadow boundaries, or the page. */
|
|
568
|
+
function scrollerOf(from: Element | null, horizontal: boolean): Element | null {
|
|
569
|
+
for (let el: Element | null = from; el; el = el.parentElement ?? ((el.getRootNode() as ShadowRoot).host ?? null)) {
|
|
570
|
+
const style = getComputedStyle(el);
|
|
571
|
+
const overflow = horizontal ? style.overflowX : style.overflowY;
|
|
572
|
+
const scrolls = horizontal ? el.scrollWidth > el.clientWidth : el.scrollHeight > el.clientHeight;
|
|
573
|
+
if (scrolls && (overflow === 'auto' || overflow === 'scroll')) return el;
|
|
574
|
+
}
|
|
575
|
+
return document.scrollingElement;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function moved(keys: string[], fromIndex: number, toIndex: number) {
|
|
579
|
+
const order = [...keys];
|
|
580
|
+
const [item] = order.splice(fromIndex, 1);
|
|
581
|
+
order.splice(toIndex, 0, item);
|
|
582
|
+
return order;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* The border radius that rounds a row's corners where it meets the corners of the nearest rounded ancestor of `host`
|
|
587
|
+
* that clips it (a flush list in a panel), so a drag placeholder's dashed outline follows the clip there instead of
|
|
588
|
+
* being cut off. Null when nothing rounded clips the row.
|
|
589
|
+
*/
|
|
590
|
+
export function clipCornerRadius(host: Element, row: HTMLElement): string | null {
|
|
591
|
+
const parent = (el: Element): Element | null =>
|
|
592
|
+
el.assignedSlot ?? el.parentElement ?? (el.getRootNode() as ShadowRoot).host ?? null;
|
|
593
|
+
|
|
594
|
+
let clip: Element | null = null;
|
|
595
|
+
for (let el = parent(host); el; el = parent(el)) {
|
|
596
|
+
const style = getComputedStyle(el);
|
|
597
|
+
if (style.overflow !== 'visible' && style.borderRadius !== '0px') {
|
|
598
|
+
clip = el;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (!clip) return null;
|
|
603
|
+
|
|
604
|
+
const style = getComputedStyle(clip);
|
|
605
|
+
const outer = clip.getBoundingClientRect();
|
|
606
|
+
const bl = parseFloat(style.borderLeftWidth);
|
|
607
|
+
const br = parseFloat(style.borderRightWidth);
|
|
608
|
+
const bt = parseFloat(style.borderTopWidth);
|
|
609
|
+
const bb = parseFloat(style.borderBottomWidth);
|
|
610
|
+
const r = row.getBoundingClientRect();
|
|
611
|
+
const top = Math.abs(r.top - (outer.top + bt)) < 1;
|
|
612
|
+
const bottom = Math.abs(r.bottom - (outer.bottom - bb)) < 1;
|
|
613
|
+
const left = Math.abs(r.left - (outer.left + bl)) < 1;
|
|
614
|
+
const right = Math.abs(r.right - (outer.right - br)) < 1;
|
|
615
|
+
// The clip follows the padding edge, whose radius is the border radius less the border width
|
|
616
|
+
const corner = (touches: boolean, radius: string, border: number) =>
|
|
617
|
+
touches ? `${Math.max(0, parseFloat(radius) - border)}px` : '0px';
|
|
618
|
+
|
|
619
|
+
return [
|
|
620
|
+
corner(top && left, style.borderTopLeftRadius, Math.max(bt, bl)),
|
|
621
|
+
corner(top && right, style.borderTopRightRadius, Math.max(bt, br)),
|
|
622
|
+
corner(bottom && right, style.borderBottomRightRadius, Math.max(bb, br)),
|
|
623
|
+
corner(bottom && left, style.borderBottomLeftRadius, Math.max(bb, bl)),
|
|
624
|
+
].join(' ');
|
|
625
|
+
}
|
package/src/internal/scroll.ts
CHANGED
|
@@ -86,9 +86,57 @@ export function getScrolledAncestors(element: Element | null | undefined): Eleme
|
|
|
86
86
|
return scrolled;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// A display: contents host has no box of its own, so it starts where its first rendered part does.
|
|
90
|
+
function topOf(host: HTMLElement): number {
|
|
91
|
+
const rect = host.getBoundingClientRect();
|
|
92
|
+
if (rect.height > 0) {
|
|
93
|
+
return rect.top;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const parts = Array.from(host.shadowRoot?.children ?? []).flatMap(child =>
|
|
97
|
+
child instanceof HTMLSlotElement ? child.assignedElements() : [child]
|
|
98
|
+
);
|
|
99
|
+
const tops = parts.map(part => part.getBoundingClientRect()).filter(box => box.height > 0).map(box => box.top);
|
|
100
|
+
return tops.length ? Math.min(...tops) : Infinity;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Shows a newly selected tab from its start. Scrollers inside `host` return to their top; scrollers around it only
|
|
105
|
+
* scroll back far enough to show the top of `host`, leaving the reader's place alone while the tabs are in view.
|
|
106
|
+
*/
|
|
107
|
+
export function revealTop(containers: Iterable<Element>, host: HTMLElement) {
|
|
91
108
|
for (const container of containers) {
|
|
92
|
-
|
|
109
|
+
if (host.shadowRoot?.contains(container)) {
|
|
110
|
+
container.scrollTo({ top: 0 });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Measured per scroller, as scrolling an inner one moves the tabs
|
|
115
|
+
const top = topOf(host);
|
|
116
|
+
const viewTop = container === document.scrollingElement ? 0 : container.getBoundingClientRect().top;
|
|
117
|
+
if (top < viewTop) {
|
|
118
|
+
container.scrollBy({ top: top - viewTop });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Scrolls each scroller around `element`, innermost first, just far enough to show its bottom edge. */
|
|
124
|
+
export function revealBottom(element: Element) {
|
|
125
|
+
let node: Node | null = element.parentNode;
|
|
126
|
+
|
|
127
|
+
while (node) {
|
|
128
|
+
if (node instanceof Element && node.scrollHeight > node.clientHeight
|
|
129
|
+
&& (node === document.scrollingElement || /auto|scroll/.test(getComputedStyle(node).overflowY))) {
|
|
130
|
+
// Measured per scroller, as scrolling an inner one moves the element
|
|
131
|
+
const viewBottom = node === document.scrollingElement
|
|
132
|
+
? window.innerHeight
|
|
133
|
+
: node.getBoundingClientRect().top + node.clientTop + node.clientHeight;
|
|
134
|
+
const overflow = element.getBoundingClientRect().bottom - viewBottom;
|
|
135
|
+
if (overflow > 0) {
|
|
136
|
+
node.scrollBy({top: overflow});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Through slots too, as a host's scroller sits in its shadow root around the slotted content
|
|
140
|
+
node = node instanceof ShadowRoot ? node.host : (node as Element).assignedSlot ?? node.parentNode;
|
|
93
141
|
}
|
|
94
142
|
}
|
|
@@ -3,7 +3,7 @@ export type SelectionCardImagePosition = 'top' | 'right' | 'bottom' | 'left';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Where a selection card renders its radio/checkbox indicator. `start` keeps the standard
|
|
6
|
-
* inline position
|
|
6
|
+
* inline position, except on a plain contained radio or checkbox which hides it like `none`.
|
|
7
7
|
*/
|
|
8
8
|
export type SelectionCardControlPosition =
|
|
9
9
|
| 'start'
|