@fusion.dev/helix-headless 0.145.0 → 0.146.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/custom-elements.json +7373 -3849
- package/dist/accordion/item/accordion-item.d.ts +14 -0
- package/dist/accordion/item/accordion-item.js +16 -6
- package/dist/accordion/root/accordion-root.d.ts +8 -5
- package/dist/announcement/root/announcement-root.d.ts +3 -2
- package/dist/announcement/root/announcement-root.js +3 -1
- package/dist/app-header/root/app-header-root.d.ts +7 -2
- package/dist/app-header/root/app-header-root.js +9 -1
- package/dist/app-header/sectioning.d.ts +3 -1
- package/dist/app-header/sectioning.js +3 -2
- package/dist/avatar/group/avatar-group.d.ts +20 -14
- package/dist/avatar/group/avatar-group.js +17 -19
- package/dist/badge/root/badge-root.d.ts +3 -1
- package/dist/badge/root/badge-root.js +2 -2
- package/dist/bar-chart/root/bar-chart-root.d.ts +18 -1
- package/dist/bar-chart/root/bar-chart-root.js +52 -15
- package/dist/bar-chart/stack.d.ts +15 -0
- package/dist/bar-chart/stack.js +19 -0
- package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +25 -0
- package/dist/breadcrumbs/root/breadcrumbs-root.js +50 -3
- package/dist/button/root/button-root.d.ts +26 -0
- package/dist/button/root/button-root.js +38 -18
- package/dist/button-group/root/button-group-root.d.ts +2 -1
- package/dist/button-group/root/button-group-root.js +5 -1
- package/dist/canvas/node/canvas-node.d.ts +4 -3
- package/dist/canvas/node/canvas-node.js +3 -2
- package/dist/canvas/root/canvas-root.js +2 -2
- package/dist/card/root/card-root.d.ts +24 -3
- package/dist/card/root/card-root.js +20 -8
- package/dist/carousel/root/carousel-root.d.ts +43 -4
- package/dist/carousel/root/carousel-root.js +40 -12
- package/dist/carousel/stops.d.ts +20 -0
- package/dist/carousel/stops.js +30 -0
- package/dist/chat/message/chat-message.js +5 -4
- package/dist/chat/naming.d.ts +5 -1
- package/dist/chat/naming.js +4 -2
- package/dist/chat/root/chat-root.d.ts +25 -3
- package/dist/chat/root/chat-root.js +22 -5
- package/dist/checkbox/root/checkbox-root.d.ts +20 -0
- package/dist/checkbox/root/checkbox-root.js +14 -1
- package/dist/checkbox-group/root/checkbox-group-root.d.ts +8 -2
- package/dist/checkbox-group/root/checkbox-group-root.js +12 -10
- package/dist/checkbox-group/select-all.d.ts +27 -0
- package/dist/checkbox-group/select-all.js +12 -0
- package/dist/code-block/highlight.d.ts +15 -0
- package/dist/code-block/highlight.js +4 -0
- package/dist/code-block/root/code-block-root.d.ts +49 -1
- package/dist/code-block/root/code-block-root.js +58 -3
- package/dist/color-input/root/color-input-root.d.ts +3 -0
- package/dist/color-input/root/color-input-root.js +6 -3
- package/dist/color-picker/root/color-picker-root.d.ts +33 -4
- package/dist/color-picker/root/color-picker-root.js +80 -21
- package/dist/color-picker/swatches.d.ts +12 -0
- package/dist/color-picker/swatches.js +26 -0
- package/dist/combobox/root/combobox-root.d.ts +43 -0
- package/dist/combobox/root/combobox-root.js +79 -15
- package/dist/command/root/command-root.d.ts +16 -1
- package/dist/command/root/command-root.js +22 -4
- package/dist/copy-button/root/copy-button-root.d.ts +5 -0
- package/dist/copy-button/root/copy-button-root.js +10 -3
- package/dist/data-table/index.d.ts +1 -1
- package/dist/data-table/index.js +1 -1
- package/dist/data-table/keys.d.ts +24 -0
- package/dist/data-table/keys.js +24 -0
- package/dist/data-table/root/data-table-root.d.ts +27 -18
- package/dist/data-table/root/data-table-root.js +20 -52
- package/dist/data-table/tree.d.ts +16 -1
- package/dist/data-table/tree.js +2 -3
- package/dist/date-input/root/date-input-root.d.ts +3 -1
- package/dist/date-input/root/date-input-root.js +18 -7
- package/dist/date-picker/root/date-picker-root.js +12 -6
- package/dist/dialog/description.js +15 -1
- package/dist/dialog/root/dialog-root.d.ts +8 -2
- package/dist/dialog/root/dialog-root.js +16 -1
- package/dist/divider/root/divider-root.d.ts +3 -0
- package/dist/divider/root/divider-root.js +2 -1
- package/dist/donut-chart/root/donut-chart-root.d.ts +31 -0
- package/dist/donut-chart/root/donut-chart-root.js +48 -1
- package/dist/drawer/reveal.d.ts +8 -0
- package/dist/drawer/reveal.js +1 -0
- package/dist/drawer/root/drawer-root.d.ts +59 -0
- package/dist/drawer/root/drawer-root.js +63 -4
- package/dist/drilldown/item/drilldown-item.js +1 -1
- package/dist/drilldown/root/drilldown-root.d.ts +21 -3
- package/dist/drilldown/root/drilldown-root.js +21 -3
- package/dist/element-events.generated.js +31 -5
- package/dist/empty/root/empty-root.d.ts +3 -1
- package/dist/empty/root/empty-root.js +3 -2
- package/dist/file-input/root/file-input-root.d.ts +69 -6
- package/dist/file-input/root/file-input-root.js +82 -19
- package/dist/file-input/screen.d.ts +2 -1
- package/dist/filter-input/menu/filter-menu.d.ts +2 -0
- package/dist/filter-input/menu/filter-menu.js +12 -4
- package/dist/filter-input/root/filter-input-root.d.ts +30 -6
- package/dist/filter-input/root/filter-input-root.js +1 -1
- package/dist/filter-input/suggest.d.ts +28 -3
- package/dist/filter-input/suggest.js +18 -8
- package/dist/format-date/root/format-date-root.d.ts +9 -16
- package/dist/format-date/root/format-date-root.js +10 -27
- package/dist/heatmap/root/heatmap-root.d.ts +24 -13
- package/dist/heatmap/root/heatmap-root.js +34 -12
- package/dist/icon/root/icon-root.d.ts +12 -1
- package/dist/icon/root/icon-root.js +21 -10
- package/dist/icon/settle.d.ts +7 -0
- package/dist/icon/settle.js +10 -0
- package/dist/icon-button/root/icon-button-root.d.ts +2 -0
- package/dist/image/root/image-root.d.ts +5 -1
- package/dist/image/root/image-root.js +1 -1
- package/dist/input/root/input-root.d.ts +12 -2
- package/dist/input/root/input-root.js +11 -2
- package/dist/kbd/root/kbd-root.d.ts +35 -1
- package/dist/kbd/root/kbd-root.js +23 -4
- package/dist/legend/root/legend-root.d.ts +55 -8
- package/dist/legend/root/legend-root.js +55 -13
- package/dist/lightbox/root/lightbox-root.d.ts +49 -6
- package/dist/lightbox/root/lightbox-root.js +86 -19
- package/dist/lightbox/zoom-controller.d.ts +59 -0
- package/dist/lightbox/zoom-controller.js +128 -0
- package/dist/lightbox/zoom.d.ts +22 -0
- package/dist/lightbox/zoom.js +16 -0
- package/dist/line-chart/root/line-chart-root.d.ts +22 -1
- package/dist/line-chart/root/line-chart-root.js +21 -1
- package/dist/list/item/list-item.d.ts +2 -0
- package/dist/list/item/list-item.js +11 -2
- package/dist/list/root/list-root.d.ts +62 -13
- package/dist/list/root/list-root.js +70 -13
- package/dist/list/selection.d.ts +37 -7
- package/dist/list/selection.js +16 -7
- package/dist/listbox/root/listbox-root.d.ts +37 -1
- package/dist/listbox/root/listbox-root.js +49 -3
- package/dist/loader/busy.d.ts +10 -0
- package/dist/loader/busy.js +11 -0
- package/dist/loader/root/loader-root.d.ts +9 -2
- package/dist/loader/root/loader-root.js +23 -3
- package/dist/megamenu/root/megamenu-root.d.ts +51 -10
- package/dist/megamenu/root/megamenu-root.js +76 -5
- package/dist/menu/item/menu-item.d.ts +51 -2
- package/dist/menu/item/menu-item.js +47 -23
- package/dist/menu/root/menu-root.d.ts +14 -0
- package/dist/menu/root/menu-root.js +2 -2
- package/dist/meter/root/meter-root.d.ts +20 -1
- package/dist/meter/root/meter-root.js +11 -15
- package/dist/navigation/root/navigation-root.d.ts +2 -1
- package/dist/navigation/root/navigation-root.js +2 -2
- package/dist/notifications/root/notifications-root.d.ts +10 -3
- package/dist/notifications/root/notifications-root.js +19 -7
- package/dist/number-input/root/number-input-root.d.ts +17 -8
- package/dist/number-input/root/number-input-root.js +2 -7
- package/dist/object-inspector/root/object-inspector-root.d.ts +75 -27
- package/dist/object-inspector/root/object-inspector-root.js +117 -76
- package/dist/object-inspector/rows.d.ts +18 -0
- package/dist/object-inspector/rows.js +33 -0
- package/dist/option/root/option-root.d.ts +5 -3
- package/dist/outline/root/outline-root.d.ts +18 -0
- package/dist/outline/root/outline-root.js +23 -5
- package/dist/pagination/size/pagination-size.d.ts +1 -0
- package/dist/pagination/size/pagination-size.js +1 -1
- package/dist/pin-input/code.d.ts +10 -0
- package/dist/pin-input/code.js +13 -0
- package/dist/pin-input/root/pin-input-root.d.ts +25 -1
- package/dist/pin-input/root/pin-input-root.js +10 -11
- package/dist/popover/hide-outside.d.ts +14 -0
- package/dist/popover/hide-outside.js +41 -0
- package/dist/popover/root/popover-root.d.ts +57 -7
- package/dist/popover/root/popover-root.js +58 -3
- package/dist/progress/root/progress-root.d.ts +20 -0
- package/dist/progress/root/progress-root.js +15 -15
- package/dist/qr-code/export.d.ts +26 -1
- package/dist/qr-code/export.js +14 -0
- package/dist/qr-code/root/qr-code-root.d.ts +19 -5
- package/dist/qr-code/root/qr-code-root.js +58 -9
- package/dist/quote/marks.d.ts +8 -0
- package/dist/quote/marks.js +5 -0
- package/dist/quote/root/quote-root.d.ts +14 -2
- package/dist/quote/root/quote-root.js +23 -1
- package/dist/radio/root/radio-root.d.ts +6 -2
- package/dist/radio/root/radio-root.js +6 -1
- package/dist/rating/root/rating-root.d.ts +9 -4
- package/dist/rating/root/rating-root.js +31 -7
- package/dist/rating/steps.d.ts +7 -0
- package/dist/rating/steps.js +9 -0
- package/dist/scatter-chart/fit.d.ts +15 -0
- package/dist/scatter-chart/fit.js +35 -0
- package/dist/scatter-chart/root/scatter-chart-root.d.ts +21 -1
- package/dist/scatter-chart/root/scatter-chart-root.js +30 -17
- package/dist/scroll-area/root/scroll-area-root.d.ts +22 -51
- package/dist/scroll-area/root/scroll-area-root.js +57 -173
- package/dist/scroll-area/scrollbar-controller.d.ts +111 -0
- package/dist/scroll-area/scrollbar-controller.js +172 -0
- package/dist/scroll-area/scrollbar.d.ts +27 -0
- package/dist/scroll-area/scrollbar.js +9 -0
- package/dist/select/root/select-root.d.ts +15 -0
- package/dist/select/root/select-root.js +18 -6
- package/dist/skeleton/root/skeleton-root.d.ts +3 -0
- package/dist/skeleton/root/skeleton-root.js +4 -2
- package/dist/skip-link/root/skip-link-root.js +2 -2
- package/dist/slider/root/slider-root.d.ts +41 -4
- package/dist/slider/root/slider-root.js +23 -3
- package/dist/sparkline/readings.d.ts +18 -0
- package/dist/sparkline/readings.js +19 -0
- package/dist/sparkline/root/sparkline-root.d.ts +78 -7
- package/dist/sparkline/root/sparkline-root.js +118 -23
- package/dist/speed-dial/root/speed-dial-root.js +2 -2
- package/dist/splitter/root/splitter-root.d.ts +7 -12
- package/dist/splitter/root/splitter-root.js +209 -204
- package/dist/stat/root/stat-root.d.ts +14 -5
- package/dist/stat/root/stat-root.js +21 -5
- package/dist/steps/reach.d.ts +5 -0
- package/dist/steps/reach.js +3 -0
- package/dist/steps/root/steps-root.d.ts +49 -3
- package/dist/steps/root/steps-root.js +44 -5
- package/dist/steps/step/step.d.ts +7 -0
- package/dist/steps/step/step.js +7 -2
- package/dist/switch/root/switch-root.d.ts +10 -0
- package/dist/table/column/table-column.d.ts +0 -10
- package/dist/table/column/table-column.js +3 -19
- package/dist/table/root/table-root.d.ts +31 -10
- package/dist/table/root/table-root.js +2 -13
- package/dist/tabs/panel/tab-panel.d.ts +16 -3
- package/dist/tabs/panel/tab-panel.js +34 -5
- package/dist/tabs/root/tabs-root.d.ts +12 -0
- package/dist/tabs/root/tabs-root.js +1 -1
- package/dist/tabs/tab/tab.d.ts +5 -0
- package/dist/tabs/tab/tab.js +5 -2
- package/dist/tag/group/tag-group.d.ts +31 -4
- package/dist/tag/group/tag-group.js +46 -7
- package/dist/tag/root/tag-root.js +9 -2
- package/dist/textarea/root/textarea-root.d.ts +28 -1
- package/dist/textarea/root/textarea-root.js +40 -1
- package/dist/time-input/root/time-input-root.d.ts +5 -1
- package/dist/time-input/root/time-input-root.js +16 -8
- package/dist/time-picker/root/time-picker-root.d.ts +18 -4
- package/dist/time-picker/root/time-picker-root.js +7 -10
- package/dist/time-picker/wheel-name.d.ts +9 -0
- package/dist/time-picker/wheel-name.js +15 -0
- package/dist/timeline/group/timeline-group.js +2 -2
- package/dist/toast/item/toast-item.d.ts +14 -1
- package/dist/toast/item/toast-item.js +22 -5
- package/dist/toast/root/toast-root.d.ts +6 -0
- package/dist/toast/root/toast-root.js +16 -1
- package/dist/toggle/group/toggle-group.d.ts +24 -3
- package/dist/toggle/group/toggle-group.js +34 -5
- package/dist/tooltip/root/tooltip-root.d.ts +13 -2
- package/dist/tooltip/root/tooltip-root.js +9 -1
- package/dist/tour/back.d.ts +10 -0
- package/dist/tour/back.js +8 -0
- package/dist/tour/root/tour-root.d.ts +30 -2
- package/dist/tour/root/tour-root.js +34 -8
- package/dist/tree/item/tree-item.d.ts +7 -3
- package/dist/tree/item/tree-item.js +3 -0
- package/dist/tree/root/tree-root.d.ts +24 -3
- package/dist/tree/root/tree-root.js +32 -13
- package/dist/tree/selection.js +2 -15
- package/dist/trend/root/trend-root.d.ts +43 -18
- package/dist/trend/root/trend-root.js +31 -6
- package/dist/video/media-events.d.ts +13 -18
- package/dist/video/media-events.js +1 -5
- package/dist/video/root/video-root.d.ts +18 -5
- package/dist/video/root/video-root.js +19 -10
- package/dist/video-playlist/menu/video-playlist-menu.d.ts +2 -0
- package/dist/video-playlist/menu/video-playlist-menu.js +2 -1
- package/dist/video-playlist/root/video-playlist-root.d.ts +7 -1
- package/dist/virtual-list/fill.d.ts +0 -8
- package/dist/virtual-list/fill.js +1 -17
- package/dist/virtual-list/plan.d.ts +49 -0
- package/dist/virtual-list/plan.js +16 -0
- package/dist/virtual-list/root/virtual-list-root.d.ts +114 -13
- package/dist/virtual-list/root/virtual-list-root.js +183 -40
- package/lint.json +1 -1
- package/package.json +8 -8
- package/types/jsx.d.ts +4577 -835
- package/types/vue.d.ts +926 -168
- package/dist/chat/relative-clock.d.ts +0 -39
- package/dist/chat/relative-clock.js +0 -31
- package/dist/data-table/root/order.d.ts +0 -141
- package/dist/data-table/root/order.js +0 -76
- package/dist/tag/selection.d.ts +0 -10
- package/dist/tag/selection.js +0 -12
|
@@ -4,9 +4,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { commaList, customElement, HelixBeforeChangeEvent, HelixElement, requestChange, SourceController, seriesIndex, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { commaList, customElement, HelixBeforeChangeEvent, HelixElement, KeyboardNavigationController, requestChange, SourceController, seriesIndex, seriesKey, typeaheadBy, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
|
-
import { property } from 'lit/decorators.js';
|
|
9
|
+
import { property, state } from 'lit/decorators.js';
|
|
10
10
|
import { isHidden, toggledNames } from '../hidden-series.js';
|
|
11
11
|
let HelixHeadlessLegend = class HelixHeadlessLegend extends HelixElement {
|
|
12
12
|
constructor() {
|
|
@@ -16,25 +16,58 @@ let HelixHeadlessLegend = class HelixHeadlessLegend extends HelixElement {
|
|
|
16
16
|
this.label = '';
|
|
17
17
|
this.toggles = false;
|
|
18
18
|
this.source = new SourceController(this);
|
|
19
|
-
this.
|
|
19
|
+
this.stop = 0;
|
|
20
|
+
this.keys = new KeyboardNavigationController(this, {
|
|
21
|
+
items: () => this.toggles
|
|
22
|
+
? [...this.renderRoot.querySelectorAll('[part="toggle"]')].map((button) => ({
|
|
23
|
+
contains: (node) => button.contains(node),
|
|
24
|
+
disabled: false,
|
|
25
|
+
focusItem: () => button.focus(),
|
|
26
|
+
select: () => { },
|
|
27
|
+
selected: false,
|
|
28
|
+
words: button.textContent?.trim() ?? '',
|
|
29
|
+
}))
|
|
30
|
+
: [],
|
|
31
|
+
origin: 'focus',
|
|
32
|
+
selects: () => false,
|
|
33
|
+
typeahead: typeaheadBy((item) => item.words ?? ''),
|
|
34
|
+
});
|
|
35
|
+
this.wroteTo = null;
|
|
36
|
+
this.writes = false;
|
|
20
37
|
}
|
|
21
38
|
willUpdate(changed) {
|
|
22
39
|
if (changed.has('for')) {
|
|
23
40
|
this.source.watch();
|
|
24
41
|
}
|
|
42
|
+
const table = this.source.element;
|
|
43
|
+
const asked = changed.has('hiddenSeries') &&
|
|
44
|
+
!(changed.get('hiddenSeries') === undefined && this.hiddenSeries === '');
|
|
45
|
+
this.writes = asked || (table !== null && table !== this.wroteTo && this.hiddenSeries !== '');
|
|
46
|
+
if (!this.writes && table !== null) {
|
|
47
|
+
this.follow();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
follow() {
|
|
51
|
+
const hidden = this.source.hidden;
|
|
52
|
+
const now = commaList(this.hiddenSeries);
|
|
53
|
+
const next = this.names().filter((name) => hidden.has(seriesKey(name)));
|
|
54
|
+
const same = next.length === now.length &&
|
|
55
|
+
next.every((name) => now.some((one) => seriesKey(one) === seriesKey(name)));
|
|
56
|
+
if (!same) {
|
|
57
|
+
this.hiddenSeries = next.join(', ');
|
|
58
|
+
}
|
|
25
59
|
}
|
|
26
60
|
updated(changed) {
|
|
27
61
|
super.updated(changed);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.source.hide(names);
|
|
31
|
-
this.wrote = names.length > 0;
|
|
62
|
+
if (this.writes) {
|
|
63
|
+
this.writes = false;
|
|
64
|
+
this.wroteTo = this.source.hide(commaList(this.hiddenSeries), this.names()) ?? this.wroteTo;
|
|
32
65
|
}
|
|
33
66
|
}
|
|
34
67
|
disconnectedCallback() {
|
|
35
|
-
if (this.
|
|
36
|
-
this.source.hide([]);
|
|
37
|
-
this.
|
|
68
|
+
if (this.wroteTo !== null) {
|
|
69
|
+
this.source.hide([], this.names());
|
|
70
|
+
this.wroteTo = null;
|
|
38
71
|
}
|
|
39
72
|
super.disconnectedCallback();
|
|
40
73
|
}
|
|
@@ -58,8 +91,10 @@ let HelixHeadlessLegend = class HelixHeadlessLegend extends HelixElement {
|
|
|
58
91
|
}
|
|
59
92
|
render() {
|
|
60
93
|
const named = this.label || this.source.element?.querySelector('caption')?.textContent?.trim();
|
|
94
|
+
const names = this.names();
|
|
95
|
+
const stop = Math.min(this.stop, names.length - 1);
|
|
61
96
|
return html `<ul aria-label=${named || nothing} part="list" role="list">
|
|
62
|
-
${
|
|
97
|
+
${names.map((name, index) => {
|
|
63
98
|
const off = isHidden(this.hiddenSeries, name);
|
|
64
99
|
return html `<li
|
|
65
100
|
data-series=${seriesIndex(index)}
|
|
@@ -67,17 +102,21 @@ let HelixHeadlessLegend = class HelixHeadlessLegend extends HelixElement {
|
|
|
67
102
|
style="--chart-series: var(--chart-series-${seriesIndex(index)})"
|
|
68
103
|
role="listitem"
|
|
69
104
|
>
|
|
70
|
-
${this.toggles ? this.renderToggle(name, off) : html `<span part="name">${name}</span>`}
|
|
105
|
+
${this.toggles ? this.renderToggle(name, off, index, index === stop) : html `<span part="name">${name}</span>`}
|
|
71
106
|
</li>`;
|
|
72
107
|
})}
|
|
73
108
|
</ul>`;
|
|
74
109
|
}
|
|
75
|
-
renderToggle(name, off) {
|
|
110
|
+
renderToggle(name, off, index = 0, holds = true) {
|
|
76
111
|
return html `<button
|
|
77
112
|
aria-pressed=${off ? 'false' : 'true'}
|
|
78
113
|
part="toggle"
|
|
114
|
+
tabindex=${holds ? '0' : '-1'}
|
|
79
115
|
type="button"
|
|
80
116
|
@click=${() => this.toggle(name)}
|
|
117
|
+
@focus=${() => {
|
|
118
|
+
this.stop = index;
|
|
119
|
+
}}
|
|
81
120
|
>
|
|
82
121
|
<span part="name">${name}</span>
|
|
83
122
|
</button>`;
|
|
@@ -98,6 +137,9 @@ __decorate([
|
|
|
98
137
|
__decorate([
|
|
99
138
|
property({ reflect: true, type: Boolean })
|
|
100
139
|
], HelixHeadlessLegend.prototype, "toggles", void 0);
|
|
140
|
+
__decorate([
|
|
141
|
+
state()
|
|
142
|
+
], HelixHeadlessLegend.prototype, "stop", void 0);
|
|
101
143
|
HelixHeadlessLegend = __decorate([
|
|
102
144
|
customElement('helix-headless-legend')
|
|
103
145
|
], HelixHeadlessLegend);
|
|
@@ -19,9 +19,18 @@ declare const HelixHeadlessLightbox_base: (new (...args: any[]) => import("@fusi
|
|
|
19
19
|
* focus stays in the dialog.
|
|
20
20
|
*
|
|
21
21
|
* Set `controls` for the viewer's own previous, next and close controls, named
|
|
22
|
-
* by `previous-label`, `next-label` and `dismiss-label
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* by `previous-label`, `next-label` and `dismiss-label`, whose glyphs you can
|
|
23
|
+
* replace in the `previous-icon`, `next-icon` and `dismiss-icon` slots. Without
|
|
24
|
+
* it, compose the chrome yourself in the `header` and `footer` slots, with
|
|
25
|
+
* buttons that send `--previous`, `--next` and `--close`.
|
|
26
|
+
*
|
|
27
|
+
* **Set `max-zoom` to let a reader zoom into the shown item**, such as `3`.
|
|
28
|
+
* Two fingers pinch it, a double click or a double tap zooms all the way in
|
|
29
|
+
* about the point pressed and back out, and once zoomed one pointer drags it
|
|
30
|
+
* about. `+` and `-` zoom in and out from the keyboard, and `0` puts it back.
|
|
31
|
+
* The scale and the offset are on the `viewport` part as `--lightbox-zoom`,
|
|
32
|
+
* `--lightbox-pan-x` and `--lightbox-pan-y`, written only while zoomed, and a
|
|
33
|
+
* swipe moves to the next item only while the item is at its own size.
|
|
25
34
|
*
|
|
26
35
|
* Each item is a slide: a `group` with the role description in
|
|
27
36
|
* `slide-role-label`, named by its `label` or by `slide-label`. A touch or a pen
|
|
@@ -41,8 +50,11 @@ declare const HelixHeadlessLightbox_base: (new (...args: any[]) => import("@fusi
|
|
|
41
50
|
* @documentation https://fusion.dev/helix/components/lightbox
|
|
42
51
|
*
|
|
43
52
|
* @slot - The items, which are `<helix-headless-lightbox-item>` elements.
|
|
53
|
+
* @slot dismiss-icon - Replaces the glyph inside the close control.
|
|
44
54
|
* @slot footer - Content below the item, such as a counter or a thumbnail strip.
|
|
45
55
|
* @slot header - Content above the item, such as a title and a close button.
|
|
56
|
+
* @slot next-icon - Replaces the glyph inside the next control.
|
|
57
|
+
* @slot previous-icon - Replaces the glyph inside the previous control.
|
|
46
58
|
*
|
|
47
59
|
* @event {Event} close - Fired when the viewer closes. The component dispatches it again from the host, because the dialog's own event does not leave the shadow root.
|
|
48
60
|
* @event {HelixActivateEvent} helix-activate - Fired after the shown item changes, when the element already holds the new item.
|
|
@@ -51,19 +63,26 @@ declare const HelixHeadlessLightbox_base: (new (...args: any[]) => import("@fusi
|
|
|
51
63
|
* @event {HelixBeforeOpenEvent} helix-before-open - Fired before the viewer opens. Cancelable.
|
|
52
64
|
* @event {ToggleEvent} toggle - Fired when the viewer opens or closes, with `newState` and `oldState`.
|
|
53
65
|
*
|
|
66
|
+
* @csspart counter - The place of the shown item in the set, drawn with `counter`.
|
|
54
67
|
* @csspart dialog - The native dialog element.
|
|
55
68
|
* @csspart dismiss - The close control, drawn with `controls`.
|
|
56
|
-
* @csspart dismiss-mark - The glyph on the close control.
|
|
69
|
+
* @csspart dismiss-mark - The glyph on the close control, where none is slotted.
|
|
57
70
|
* @csspart live - The live region that announces which item is shown.
|
|
58
71
|
* @csspart next - The control that shows the next item, drawn with `controls`.
|
|
59
|
-
* @csspart next-mark - The glyph on the next control.
|
|
72
|
+
* @csspart next-mark - The glyph on the next control, where none is slotted.
|
|
60
73
|
* @csspart previous - The control that shows the previous item, drawn with `controls`.
|
|
61
|
-
* @csspart previous-mark - The glyph on the previous control.
|
|
74
|
+
* @csspart previous-mark - The glyph on the previous control, where none is slotted.
|
|
62
75
|
* @csspart viewport - The box the current item is shown in.
|
|
63
76
|
*
|
|
77
|
+
* @cssproperty --lightbox-pan-x - How far the zoomed item is moved across from the middle of the viewport, in pixels, toward the right. Written only while zoomed.
|
|
78
|
+
* @cssproperty --lightbox-pan-y - How far the zoomed item is moved down from the middle of the viewport, in pixels. Written only while zoomed.
|
|
79
|
+
* @cssproperty --lightbox-zoom - The scale the shown item is zoomed to, such as `2`. Written only while zoomed.
|
|
80
|
+
*
|
|
64
81
|
* @cssstate fullscreen - While the viewer fills the screen.
|
|
65
82
|
* @cssstate idle - The user has been still long enough for the controls to hide.
|
|
66
83
|
* @cssstate open - While the viewer is open.
|
|
84
|
+
* @cssstate zoomable - While `max-zoom` lets a reader zoom.
|
|
85
|
+
* @cssstate zoomed - While the shown item is zoomed past its own size.
|
|
67
86
|
*/
|
|
68
87
|
export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
|
|
69
88
|
/**
|
|
@@ -75,6 +94,13 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
|
|
|
75
94
|
* either end of a set that does not `loop`.
|
|
76
95
|
*/
|
|
77
96
|
controls: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Draws the place of the shown item in the set over the picture, in the
|
|
99
|
+
* words of `status-label`, such as "2 of 5". It hides with the controls while
|
|
100
|
+
* the reader is idle. Screen readers hear the place from the live region, so
|
|
101
|
+
* the drawn copy is hidden from them.
|
|
102
|
+
*/
|
|
103
|
+
counter: boolean;
|
|
78
104
|
/**
|
|
79
105
|
* The close control's accessible name. Translate it with the rest of your
|
|
80
106
|
* interface.
|
|
@@ -97,6 +123,11 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
|
|
|
97
123
|
* Whether moving past the last item returns to the first.
|
|
98
124
|
*/
|
|
99
125
|
loop: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* How far a reader can zoom into the shown item, as a scale, such as `3`.
|
|
128
|
+
* `1`, the default, turns zooming off.
|
|
129
|
+
*/
|
|
130
|
+
maxZoom: number;
|
|
100
131
|
/**
|
|
101
132
|
* The next control's accessible name.
|
|
102
133
|
*/
|
|
@@ -132,6 +163,14 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
|
|
|
132
163
|
/** What the live region is currently saying. */
|
|
133
164
|
private announcement;
|
|
134
165
|
protected readonly swipes: SwipeController;
|
|
166
|
+
private readonly zoomer;
|
|
167
|
+
/** Writes the zoom on the viewport, and nothing at the item's own size. */
|
|
168
|
+
private drawZoom;
|
|
169
|
+
/**
|
|
170
|
+
* Zooms the shown item to `scale`, held between `1` and `max-zoom`, about
|
|
171
|
+
* the middle of the viewport.
|
|
172
|
+
*/
|
|
173
|
+
zoomTo(scale: number): void;
|
|
135
174
|
private readonly fullscreen;
|
|
136
175
|
private readonly idle;
|
|
137
176
|
/**
|
|
@@ -202,6 +241,8 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
|
|
|
202
241
|
* a reader having no way to know which one this component thinks it is.
|
|
203
242
|
*/
|
|
204
243
|
private readonly handleKeydown;
|
|
244
|
+
/** `+` and `-` zoom in and out by half again, and `0` puts the item back. Returns whether it was one. */
|
|
245
|
+
private zoomKey;
|
|
205
246
|
/** Home and End, which are an index rather than a step. */
|
|
206
247
|
private toEnd;
|
|
207
248
|
/** The item on screen: the one `active` names, or the first where it names none. */
|
|
@@ -236,6 +277,8 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
|
|
|
236
277
|
* every page load.
|
|
237
278
|
*/
|
|
238
279
|
private say;
|
|
280
|
+
/** The shown item's place in the set, in the words of `status-label`. */
|
|
281
|
+
private place;
|
|
239
282
|
protected render(): TemplateResult;
|
|
240
283
|
/**
|
|
241
284
|
* The viewer's own controls, behind a seam a tier above can move.
|
|
@@ -4,20 +4,23 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { CommandController, customElement, deepActiveElement, deepContains, FullscreenController, fillTemplate, HelixElement, hostAria, IdleController, inlineStep, isRtl, keyClaimed, lockScroll, MixinControllerActive, MixinControllerOpen, resolveActiveItem, SwipeController, swipeStep, unlockScroll, whenHydrated, } from '@fusion.dev/helix-core';
|
|
8
|
-
import { caretLeft, caretRight
|
|
7
|
+
import { CommandController, customElement, deepActiveElement, deepContains, drawDismiss, drawDismissGlyph, FullscreenController, fillTemplate, HelixElement, hostAria, IdleController, inlineStep, isRtl, keyClaimed, lockScroll, MixinControllerActive, MixinControllerOpen, resolveActiveItem, SwipeController, swipeStep, unlockScroll, whenHydrated, } from '@fusion.dev/helix-core';
|
|
8
|
+
import { caretLeft, caretRight } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
11
11
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
12
12
|
import { HelixHeadlessLightboxItem } from '../item/lightbox-item.js';
|
|
13
|
+
import { ZoomController } from '../zoom-controller.js';
|
|
13
14
|
let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerActive(MixinControllerOpen(HelixElement)) {
|
|
14
15
|
constructor() {
|
|
15
16
|
super(...arguments);
|
|
16
17
|
this.controls = false;
|
|
18
|
+
this.counter = false;
|
|
17
19
|
this.dismissLabel = 'Close';
|
|
18
20
|
this.idleDelay = 0;
|
|
19
21
|
this.label = '';
|
|
20
22
|
this.loop = false;
|
|
23
|
+
this.maxZoom = 1;
|
|
21
24
|
this.nextLabel = 'Next';
|
|
22
25
|
this.previousLabel = 'Previous';
|
|
23
26
|
this.slideLabel = '{index} of {count}';
|
|
@@ -27,8 +30,11 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
27
30
|
this.filled = false;
|
|
28
31
|
this.announcement = '';
|
|
29
32
|
this.swipes = new SwipeController(this, {
|
|
30
|
-
axis: () => (this.open ? 'x' : undefined),
|
|
33
|
+
axis: () => (this.open && !this.zoomer.engaged ? 'x' : undefined),
|
|
31
34
|
end: (dx, _dy, vx) => {
|
|
35
|
+
if (this.zoomer.pinched) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
32
38
|
const width = this.partNamed('viewport')?.getBoundingClientRect().width ?? 0;
|
|
33
39
|
const step = swipeStep(dx, vx, width, isRtl(this));
|
|
34
40
|
if (step !== 0) {
|
|
@@ -39,6 +45,11 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
39
45
|
start: (_target, event) => event.pointerType !== 'mouse',
|
|
40
46
|
surface: () => this.dialog,
|
|
41
47
|
});
|
|
48
|
+
this.zoomer = new ZoomController(this, {
|
|
49
|
+
max: () => (this.open ? this.maxZoom : 1),
|
|
50
|
+
onChange: (scale, pan) => this.drawZoom(scale, pan),
|
|
51
|
+
surface: () => this.partNamed('viewport'),
|
|
52
|
+
});
|
|
42
53
|
this.fullscreen = new FullscreenController(this, (full) => {
|
|
43
54
|
this.filled = full;
|
|
44
55
|
});
|
|
@@ -70,6 +81,10 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
70
81
|
if (keyClaimed(event) || event.altKey || event.ctrlKey || event.metaKey) {
|
|
71
82
|
return;
|
|
72
83
|
}
|
|
84
|
+
if (this.zoomKey(event.key)) {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
73
88
|
const along = inlineStep(this, event.key);
|
|
74
89
|
if (along === 1 || event.key === 'ArrowDown') {
|
|
75
90
|
event.preventDefault();
|
|
@@ -103,6 +118,18 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
103
118
|
this.requestOpen(false);
|
|
104
119
|
};
|
|
105
120
|
}
|
|
121
|
+
drawZoom(scale, pan) {
|
|
122
|
+
const viewport = this.partNamed('viewport');
|
|
123
|
+
const zoomed = scale > 1;
|
|
124
|
+
viewport?.style.setProperty('--lightbox-zoom', zoomed ? String(scale) : '');
|
|
125
|
+
viewport?.style.setProperty('--lightbox-pan-x', zoomed ? `${Math.round(pan.x)}px` : '');
|
|
126
|
+
viewport?.style.setProperty('--lightbox-pan-y', zoomed ? `${Math.round(pan.y)}px` : '');
|
|
127
|
+
this.toggleState('zoomed', zoomed);
|
|
128
|
+
this.idle.stir();
|
|
129
|
+
}
|
|
130
|
+
zoomTo(scale) {
|
|
131
|
+
this.zoomer.zoomTo(scale);
|
|
132
|
+
}
|
|
106
133
|
chromeHasFocus() {
|
|
107
134
|
const active = deepActiveElement();
|
|
108
135
|
const own = this.shadowRoot?.activeElement?.matches('[part="previous"], [part="next"], [part="dismiss"]');
|
|
@@ -147,6 +174,25 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
147
174
|
this.requestActive(target.value);
|
|
148
175
|
}
|
|
149
176
|
}
|
|
177
|
+
zoomKey(key) {
|
|
178
|
+
if (this.maxZoom <= 1) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const scale = this.zoomer.scale;
|
|
182
|
+
if (key === '+' || key === '=') {
|
|
183
|
+
this.zoomer.zoomTo(scale * 1.5);
|
|
184
|
+
}
|
|
185
|
+
else if (key === '-') {
|
|
186
|
+
this.zoomer.zoomTo(scale / 1.5);
|
|
187
|
+
}
|
|
188
|
+
else if (key === '0') {
|
|
189
|
+
this.zoomer.reset();
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
150
196
|
toEnd(at) {
|
|
151
197
|
const target = this.items[at];
|
|
152
198
|
if (target && target !== this.shown) {
|
|
@@ -161,6 +207,10 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
161
207
|
hostAria(this.internals, { label: this.label });
|
|
162
208
|
this.toggleState('fullscreen', this.filled);
|
|
163
209
|
this.toggleState('idle', this.resting);
|
|
210
|
+
this.toggleState('zoomable', this.maxZoom > 1);
|
|
211
|
+
if (changed.has('active') || (changed.has('open') && !this.open)) {
|
|
212
|
+
this.zoomer.reset();
|
|
213
|
+
}
|
|
164
214
|
this.say(changed);
|
|
165
215
|
}
|
|
166
216
|
disconnectedCallback() {
|
|
@@ -230,6 +280,17 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
230
280
|
label: current.label,
|
|
231
281
|
});
|
|
232
282
|
}
|
|
283
|
+
place() {
|
|
284
|
+
const items = this.items;
|
|
285
|
+
const current = resolveActiveItem(this.active, items);
|
|
286
|
+
return current
|
|
287
|
+
? fillTemplate(this.statusLabel, {
|
|
288
|
+
count: items.length,
|
|
289
|
+
index: items.indexOf(current) + 1,
|
|
290
|
+
label: current.label,
|
|
291
|
+
})
|
|
292
|
+
: '';
|
|
293
|
+
}
|
|
233
294
|
render() {
|
|
234
295
|
return html `<dialog
|
|
235
296
|
part="dialog"
|
|
@@ -250,6 +311,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
250
311
|
</div>
|
|
251
312
|
<slot name="footer"></slot>
|
|
252
313
|
${this.controls ? this.renderControls() : nothing}
|
|
314
|
+
${this.counter ? html `<span part="counter" aria-hidden="true">${this.place()}</span>` : nothing}
|
|
253
315
|
<span part="live" role="status" aria-live="polite">${this.announcement || nothing}</span>
|
|
254
316
|
</dialog>`;
|
|
255
317
|
}
|
|
@@ -261,9 +323,11 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
261
323
|
aria-disabled=${this.atEnd(-1) ? 'true' : nothing}
|
|
262
324
|
@click=${() => this.previous()}
|
|
263
325
|
>
|
|
264
|
-
<
|
|
265
|
-
|
|
266
|
-
|
|
326
|
+
<slot name="previous-icon">
|
|
327
|
+
<svg part="previous-mark" data-mirror fill="currentColor" viewBox=${caretLeft.viewBox} aria-hidden="true">
|
|
328
|
+
${unsafeSVG(caretLeft.content)}
|
|
329
|
+
</svg>
|
|
330
|
+
</slot>
|
|
267
331
|
</button>
|
|
268
332
|
<button
|
|
269
333
|
part="next"
|
|
@@ -272,25 +336,25 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
272
336
|
aria-disabled=${this.atEnd(1) ? 'true' : nothing}
|
|
273
337
|
@click=${() => this.next()}
|
|
274
338
|
>
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
339
|
+
<slot name="next-icon">
|
|
340
|
+
<svg part="next-mark" data-mirror fill="currentColor" viewBox=${caretRight.viewBox} aria-hidden="true">
|
|
341
|
+
${unsafeSVG(caretRight.content)}
|
|
342
|
+
</svg>
|
|
343
|
+
</slot>
|
|
278
344
|
</button>
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
>
|
|
285
|
-
<svg part="dismiss-mark" fill="currentColor" viewBox=${x.viewBox} aria-hidden="true">
|
|
286
|
-
${unsafeSVG(x.content)}
|
|
287
|
-
</svg>
|
|
288
|
-
</button>`;
|
|
345
|
+
${drawDismiss({
|
|
346
|
+
label: this.dismissLabel,
|
|
347
|
+
mark: html `<slot name="dismiss-icon">${drawDismissGlyph()}</slot>`,
|
|
348
|
+
onDismiss: () => this.close(),
|
|
349
|
+
})}`;
|
|
289
350
|
}
|
|
290
351
|
};
|
|
291
352
|
__decorate([
|
|
292
353
|
property({ reflect: true, type: Boolean })
|
|
293
354
|
], HelixHeadlessLightbox.prototype, "controls", void 0);
|
|
355
|
+
__decorate([
|
|
356
|
+
property({ reflect: true, type: Boolean })
|
|
357
|
+
], HelixHeadlessLightbox.prototype, "counter", void 0);
|
|
294
358
|
__decorate([
|
|
295
359
|
property({ attribute: 'dismiss-label' })
|
|
296
360
|
], HelixHeadlessLightbox.prototype, "dismissLabel", void 0);
|
|
@@ -303,6 +367,9 @@ __decorate([
|
|
|
303
367
|
__decorate([
|
|
304
368
|
property({ reflect: true, type: Boolean })
|
|
305
369
|
], HelixHeadlessLightbox.prototype, "loop", void 0);
|
|
370
|
+
__decorate([
|
|
371
|
+
property({ attribute: 'max-zoom', type: Number })
|
|
372
|
+
], HelixHeadlessLightbox.prototype, "maxZoom", void 0);
|
|
306
373
|
__decorate([
|
|
307
374
|
property({ attribute: 'next-label' })
|
|
308
375
|
], HelixHeadlessLightbox.prototype, "nextLabel", void 0);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
import { type Offset } from './zoom.js';
|
|
3
|
+
export interface ZoomOptions {
|
|
4
|
+
/** The most the item may grow, as a scale. 1 or less turns zooming off. */
|
|
5
|
+
max: () => number;
|
|
6
|
+
/** Told every time the scale or the position changes. */
|
|
7
|
+
onChange: (scale: number, pan: Offset) => void;
|
|
8
|
+
/** The box the item is shown in, which presses must land in and the item scales from the middle of. */
|
|
9
|
+
surface: () => HTMLElement | null | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Pinch, double press and drag to zoom into the item a viewer shows, and to
|
|
13
|
+
* move about it once zoomed.
|
|
14
|
+
*
|
|
15
|
+
* Two fingers scale the item about the point between them. A double press, a
|
|
16
|
+
* double click or a double tap, zooms all the way in about the point pressed,
|
|
17
|
+
* and back out again. One pointer dragged across a zoomed item moves it, never
|
|
18
|
+
* so far that an edge comes inside the box. What it reports is a scale and an
|
|
19
|
+
* offset from the middle of the box; drawing them is the caller's.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ZoomController implements ReactiveController {
|
|
22
|
+
private readonly host;
|
|
23
|
+
private readonly options;
|
|
24
|
+
/** The pointers down on the box, where each one is now. */
|
|
25
|
+
private readonly pointers;
|
|
26
|
+
/** What the gesture under way started from. */
|
|
27
|
+
private from;
|
|
28
|
+
/** The scale, 1 at rest. */
|
|
29
|
+
scale: number;
|
|
30
|
+
/** How far the item is moved from the middle of the box, in pixels. */
|
|
31
|
+
pan: Offset;
|
|
32
|
+
/**
|
|
33
|
+
* Whether two fingers have been down since the last gesture began, so a
|
|
34
|
+
* swipe that started with the first of them can be told it was a pinch.
|
|
35
|
+
*/
|
|
36
|
+
pinched: boolean;
|
|
37
|
+
constructor(host: ReactiveControllerHost & HTMLElement, options: ZoomOptions);
|
|
38
|
+
/** Whether the item is zoomed, or two fingers are on it: either way, a drag is not a swipe. */
|
|
39
|
+
get engaged(): boolean;
|
|
40
|
+
hostConnected(): void;
|
|
41
|
+
hostDisconnected(): void;
|
|
42
|
+
/** Zooms to `scale` about the middle of the box, held to the range. */
|
|
43
|
+
zoomTo(scale: number, at?: Offset): void;
|
|
44
|
+
/** Back to the item at its own size, in the middle. */
|
|
45
|
+
reset(): void;
|
|
46
|
+
private set;
|
|
47
|
+
/** A point on the page, measured from the middle of the box. */
|
|
48
|
+
private offset;
|
|
49
|
+
/** Whether a press landed in the box, through slotted content as well. */
|
|
50
|
+
private inside;
|
|
51
|
+
/** Starts over from where every pointer down is now. */
|
|
52
|
+
private begin;
|
|
53
|
+
private readonly handleDown;
|
|
54
|
+
private readonly handleMove;
|
|
55
|
+
private readonly handleUp;
|
|
56
|
+
/** A double press zooms all the way in about the point, or back out. */
|
|
57
|
+
private readonly handleDouble;
|
|
58
|
+
private release;
|
|
59
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { between, clampPan, clampScale, spread, zoomAt } from './zoom.js';
|
|
2
|
+
export class ZoomController {
|
|
3
|
+
constructor(host, options) {
|
|
4
|
+
this.pointers = new Map();
|
|
5
|
+
this.scale = 1;
|
|
6
|
+
this.pan = { x: 0, y: 0 };
|
|
7
|
+
this.pinched = false;
|
|
8
|
+
this.handleDown = (event) => {
|
|
9
|
+
if (this.options.max() <= 1 || event.button !== 0 || !this.inside(event)) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (this.pointers.size === 0) {
|
|
13
|
+
this.pinched = false;
|
|
14
|
+
window.addEventListener('pointermove', this.handleMove);
|
|
15
|
+
window.addEventListener('pointerup', this.handleUp);
|
|
16
|
+
window.addEventListener('pointercancel', this.handleUp);
|
|
17
|
+
}
|
|
18
|
+
this.pointers.set(event.pointerId, this.offset(event));
|
|
19
|
+
if (this.pointers.size > 1) {
|
|
20
|
+
this.pinched = true;
|
|
21
|
+
}
|
|
22
|
+
this.begin();
|
|
23
|
+
};
|
|
24
|
+
this.handleMove = (event) => {
|
|
25
|
+
const from = this.from;
|
|
26
|
+
if (!from || !this.pointers.has(event.pointerId)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.pointers.set(event.pointerId, this.offset(event));
|
|
30
|
+
const [a, b] = [...this.pointers.values()];
|
|
31
|
+
if (a && b && from.spread > 0) {
|
|
32
|
+
const at = between(a, b);
|
|
33
|
+
const scale = clampScale((from.scale * spread(a, b)) / from.spread, this.options.max());
|
|
34
|
+
const about = zoomAt(from.at, from.scale, scale, from.pan);
|
|
35
|
+
this.set(scale, { x: about.x + at.x - from.at.x, y: about.y + at.y - from.at.y });
|
|
36
|
+
}
|
|
37
|
+
else if (a && this.scale > 1) {
|
|
38
|
+
this.set(this.scale, { x: from.pan.x + a.x - from.at.x, y: from.pan.y + a.y - from.at.y });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
this.handleUp = (event) => {
|
|
42
|
+
if (!this.pointers.delete(event.pointerId)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (this.pointers.size === 0) {
|
|
46
|
+
this.release();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.begin();
|
|
50
|
+
};
|
|
51
|
+
this.handleDouble = (event) => {
|
|
52
|
+
if (this.options.max() <= 1 || !this.inside(event)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (this.scale > 1) {
|
|
56
|
+
this.reset();
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.zoomTo(this.options.max(), this.offset(event));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
this.host = host;
|
|
63
|
+
this.options = options;
|
|
64
|
+
host.addController(this);
|
|
65
|
+
}
|
|
66
|
+
get engaged() {
|
|
67
|
+
return this.scale > 1 || this.pointers.size > 1;
|
|
68
|
+
}
|
|
69
|
+
hostConnected() {
|
|
70
|
+
this.host.addEventListener('pointerdown', this.handleDown);
|
|
71
|
+
this.host.addEventListener('dblclick', this.handleDouble);
|
|
72
|
+
}
|
|
73
|
+
hostDisconnected() {
|
|
74
|
+
this.host.removeEventListener('pointerdown', this.handleDown);
|
|
75
|
+
this.host.removeEventListener('dblclick', this.handleDouble);
|
|
76
|
+
this.release();
|
|
77
|
+
}
|
|
78
|
+
zoomTo(scale, at = { x: 0, y: 0 }) {
|
|
79
|
+
const next = clampScale(scale, this.options.max());
|
|
80
|
+
this.set(next, zoomAt(at, this.scale, next, this.pan));
|
|
81
|
+
}
|
|
82
|
+
reset() {
|
|
83
|
+
if (this.scale !== 1 || this.pan.x !== 0 || this.pan.y !== 0) {
|
|
84
|
+
this.set(1, { x: 0, y: 0 });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
set(scale, pan) {
|
|
88
|
+
const box = this.options.surface()?.getBoundingClientRect();
|
|
89
|
+
const held = clampPan(pan, scale, box?.width ?? 0, box?.height ?? 0);
|
|
90
|
+
if (scale === this.scale && held.x === this.pan.x && held.y === this.pan.y) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.scale = scale;
|
|
94
|
+
this.pan = held;
|
|
95
|
+
this.options.onChange(scale, held);
|
|
96
|
+
}
|
|
97
|
+
offset(event) {
|
|
98
|
+
const box = this.options.surface()?.getBoundingClientRect();
|
|
99
|
+
return {
|
|
100
|
+
x: event.clientX - (box ? box.left + box.width / 2 : 0),
|
|
101
|
+
y: event.clientY - (box ? box.top + box.height / 2 : 0),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
inside(event) {
|
|
105
|
+
const surface = this.options.surface();
|
|
106
|
+
return surface !== null && surface !== undefined && event.composedPath().includes(surface);
|
|
107
|
+
}
|
|
108
|
+
begin() {
|
|
109
|
+
const [a, b] = [...this.pointers.values()];
|
|
110
|
+
if (!a) {
|
|
111
|
+
this.from = undefined;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.from = {
|
|
115
|
+
at: b ? between(a, b) : a,
|
|
116
|
+
pan: { ...this.pan },
|
|
117
|
+
scale: this.scale,
|
|
118
|
+
spread: b ? spread(a, b) : 0,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
release() {
|
|
122
|
+
window.removeEventListener('pointermove', this.handleMove);
|
|
123
|
+
window.removeEventListener('pointerup', this.handleUp);
|
|
124
|
+
window.removeEventListener('pointercancel', this.handleUp);
|
|
125
|
+
this.pointers.clear();
|
|
126
|
+
this.from = undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** A distance from the middle of the viewer, or a point measured from it, in pixels. */
|
|
2
|
+
export interface Offset {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
/** A scale held between 1, the item at its own size, and `max`. */
|
|
7
|
+
export declare const clampScale: (scale: number, max: number) => number;
|
|
8
|
+
/**
|
|
9
|
+
* How far a zoomed item may move, kept so its edges never come inside the
|
|
10
|
+
* viewer's: at a scale of 2 in a box 400 wide, 200 either way.
|
|
11
|
+
*/
|
|
12
|
+
export declare const clampPan: (pan: Offset, scale: number, width: number, height: number) => Offset;
|
|
13
|
+
/**
|
|
14
|
+
* Where the item moves to as its scale goes from `from` to `to`, so the point
|
|
15
|
+
* under `at` stays under it. Both are measured from the middle of the viewer,
|
|
16
|
+
* which is where the item scales from.
|
|
17
|
+
*/
|
|
18
|
+
export declare const zoomAt: (at: Offset, from: number, to: number, pan: Offset) => Offset;
|
|
19
|
+
/** The distance between two fingers. */
|
|
20
|
+
export declare const spread: (a: Offset, b: Offset) => number;
|
|
21
|
+
/** The point halfway between two fingers. */
|
|
22
|
+
export declare const between: (a: Offset, b: Offset) => Offset;
|