@expressivecss/expressive 0.12.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +339 -1
- package/README.md +38 -1
- package/dist/css/expressive.css +5342 -105
- package/dist/css/expressive.css.map +1 -1
- package/dist/css/expressive.min.css +1 -1
- package/dist/css/expressive.min.css.map +1 -1
- package/dist/fonts/material-symbols-outlined.woff2 +0 -0
- package/dist/fonts/material-symbols-rounded.woff2 +0 -0
- package/dist/fonts/material-symbols-sharp.woff2 +0 -0
- package/dist/js/chunks/chunk-3DHK5CZU.mjs +105 -0
- package/dist/js/chunks/chunk-3DHK5CZU.mjs.map +7 -0
- package/dist/js/chunks/{chunk-HB6IW7QQ.mjs → chunk-3GVFZ4XO.mjs} +3 -3
- package/dist/js/chunks/{chunk-HLDXZ7XB.mjs → chunk-4HZMB7LI.mjs} +22 -8
- package/dist/js/chunks/chunk-4HZMB7LI.mjs.map +7 -0
- package/dist/js/chunks/chunk-4KMNFIFZ.mjs +270 -0
- package/dist/js/chunks/chunk-4KMNFIFZ.mjs.map +7 -0
- package/dist/js/chunks/chunk-4TKTLGEM.mjs +262 -0
- package/dist/js/chunks/chunk-4TKTLGEM.mjs.map +7 -0
- package/dist/js/chunks/{chunk-3VOK6J7U.mjs → chunk-5BKHWHYY.mjs} +13 -6
- package/dist/js/chunks/chunk-5BKHWHYY.mjs.map +7 -0
- package/dist/js/chunks/{chunk-H2WA4XHH.mjs → chunk-5HQSGDZ5.mjs} +16 -7
- package/dist/js/chunks/chunk-5HQSGDZ5.mjs.map +7 -0
- package/dist/js/chunks/{chunk-LWBQIX5J.mjs → chunk-A7ALMYUR.mjs} +41 -44
- package/dist/js/chunks/chunk-A7ALMYUR.mjs.map +7 -0
- package/dist/js/chunks/chunk-BG6ZRB5H.mjs +154 -0
- package/dist/js/chunks/chunk-BG6ZRB5H.mjs.map +7 -0
- package/dist/js/chunks/{chunk-DLWYKFEI.mjs → chunk-CUQPB25U.mjs} +3 -3
- package/dist/js/chunks/{chunk-6WLSDZ4O.mjs → chunk-DAIU5SLG.mjs} +2 -2
- package/dist/js/chunks/chunk-FS7XY3HT.mjs +124 -0
- package/dist/js/chunks/chunk-FS7XY3HT.mjs.map +7 -0
- package/dist/js/chunks/{chunk-WJXDBPXQ.mjs → chunk-GIW4K4SE.mjs} +8 -12
- package/dist/js/chunks/chunk-GIW4K4SE.mjs.map +7 -0
- package/dist/js/chunks/chunk-GUB3TNO2.mjs +164 -0
- package/dist/js/chunks/chunk-GUB3TNO2.mjs.map +7 -0
- package/dist/js/chunks/chunk-GVGT6MIU.mjs +47 -0
- package/dist/js/chunks/chunk-GVGT6MIU.mjs.map +7 -0
- package/dist/js/chunks/{chunk-HNFSMIBY.mjs → chunk-GZKEFWPM.mjs} +83 -22
- package/dist/js/chunks/chunk-GZKEFWPM.mjs.map +7 -0
- package/dist/js/chunks/chunk-HKT2S7VM.mjs +48 -0
- package/dist/js/chunks/chunk-HKT2S7VM.mjs.map +7 -0
- package/dist/js/chunks/chunk-ILP3PDTL.mjs +187 -0
- package/dist/js/chunks/chunk-ILP3PDTL.mjs.map +7 -0
- package/dist/js/chunks/{chunk-YSBCHYXP.mjs → chunk-JIDMQGK6.mjs} +118 -13
- package/dist/js/chunks/chunk-JIDMQGK6.mjs.map +7 -0
- package/dist/js/chunks/chunk-KBNN3QDH.mjs +7 -0
- package/dist/js/chunks/chunk-KBNN3QDH.mjs.map +7 -0
- package/dist/js/chunks/{chunk-NNYP33NB.mjs → chunk-KHV6DZXX.mjs} +5 -4
- package/dist/js/chunks/chunk-KHV6DZXX.mjs.map +7 -0
- package/dist/js/chunks/{chunk-RFZZCYVU.mjs → chunk-KY6U46SM.mjs} +3 -3
- package/dist/js/chunks/{chunk-CKQP6P76.mjs → chunk-LRZMJZ3T.mjs} +4 -3
- package/dist/js/chunks/chunk-LRZMJZ3T.mjs.map +7 -0
- package/dist/js/chunks/{chunk-6OMRGML6.mjs → chunk-MNPROCZF.mjs} +4 -4
- package/dist/js/chunks/{chunk-6OMRGML6.mjs.map → chunk-MNPROCZF.mjs.map} +2 -2
- package/dist/js/chunks/{chunk-QV2JAWIZ.mjs → chunk-MZ6Z24BX.mjs} +3 -2
- package/dist/js/chunks/{chunk-QV2JAWIZ.mjs.map → chunk-MZ6Z24BX.mjs.map} +2 -2
- package/dist/js/chunks/chunk-NS2UMNZV.mjs +292 -0
- package/dist/js/chunks/chunk-NS2UMNZV.mjs.map +7 -0
- package/dist/js/chunks/chunk-O5XPWXIQ.mjs +406 -0
- package/dist/js/chunks/chunk-O5XPWXIQ.mjs.map +7 -0
- package/dist/js/chunks/{chunk-UBLRRJ7N.mjs → chunk-PM655FXL.mjs} +2 -2
- package/dist/js/chunks/{chunk-IFXVDDDH.mjs → chunk-QERLECVU.mjs} +3 -3
- package/dist/js/chunks/{chunk-GZWPXXY6.mjs → chunk-QJABDLSD.mjs} +5 -2
- package/dist/js/chunks/chunk-QJABDLSD.mjs.map +7 -0
- package/dist/js/chunks/chunk-RHRQKTTX.mjs +139 -0
- package/dist/js/chunks/chunk-RHRQKTTX.mjs.map +7 -0
- package/dist/js/chunks/{chunk-UNEMYWEW.mjs → chunk-S3XDFGSZ.mjs} +2 -2
- package/dist/js/chunks/chunk-SPETGK25.mjs +59 -0
- package/dist/js/chunks/chunk-SPETGK25.mjs.map +7 -0
- package/dist/js/chunks/chunk-TBM3FT7G.mjs +398 -0
- package/dist/js/chunks/chunk-TBM3FT7G.mjs.map +7 -0
- package/dist/js/chunks/chunk-UEXCIMJZ.mjs +214 -0
- package/dist/js/chunks/chunk-UEXCIMJZ.mjs.map +7 -0
- package/dist/js/chunks/chunk-VGZSAFTX.mjs +173 -0
- package/dist/js/chunks/chunk-VGZSAFTX.mjs.map +7 -0
- package/dist/js/chunks/{chunk-UAEAQBRC.mjs → chunk-X7OCHZVF.mjs} +2 -2
- package/dist/js/chunks/{chunk-EC63CHXU.mjs → chunk-YMSXJON6.mjs} +7 -6
- package/dist/js/chunks/chunk-YMSXJON6.mjs.map +7 -0
- package/dist/js/chunks/{chunk-QVVEWBOF.mjs → chunk-Z2NRKI7R.mjs} +2 -2
- package/dist/js/chunks/{chunk-PIJ2TAR3.mjs → chunk-ZAS25YFD.mjs} +2 -2
- package/dist/js/chunks/chunk-ZKZKNSLC.mjs +1 -0
- package/dist/js/expressive.cjs +3113 -148
- package/dist/js/expressive.cjs.map +4 -4
- package/dist/js/expressive.js +3113 -148
- package/dist/js/expressive.js.map +4 -4
- package/dist/js/expressive.min.js +11 -11
- package/dist/js/expressive.min.js.map +4 -4
- package/dist/js/expressive.mjs +95 -35
- package/dist/js/expressive.mjs.map +1 -1
- package/dist/js/internal/behaviors/bottomSheets.mjs +3 -3
- package/dist/js/internal/behaviors/dialogs.mjs +2 -2
- package/dist/js/internal/behaviors/forms.mjs +2 -2
- package/dist/js/internal/behaviors/sheetDrag.mjs +2 -2
- package/dist/js/internal/behaviors/sideSheets.mjs +3 -3
- package/dist/js/internal/components/autocomplete.mjs +3 -3
- package/dist/js/internal/components/barChart.mjs +10 -0
- package/dist/js/internal/components/barChart.mjs.map +7 -0
- package/dist/js/internal/components/buttons.mjs +2 -2
- package/dist/js/internal/components/cards.mjs +2 -2
- package/dist/js/internal/components/carousel.mjs +2 -2
- package/dist/js/internal/components/chips.mjs +4 -4
- package/dist/js/internal/components/columnChart.mjs +9 -0
- package/dist/js/internal/components/columnChart.mjs.map +7 -0
- package/dist/js/internal/components/commandPalette.mjs +9 -0
- package/dist/js/internal/components/commandPalette.mjs.map +7 -0
- package/dist/js/internal/components/confetti.mjs +8 -0
- package/dist/js/internal/components/confetti.mjs.map +7 -0
- package/dist/js/internal/components/datepicker.mjs +5 -5
- package/dist/js/internal/components/expandingCard.mjs +2 -2
- package/dist/js/internal/components/heatmapChart.mjs +9 -0
- package/dist/js/internal/components/heatmapChart.mjs.map +7 -0
- package/dist/js/internal/components/index.mjs +88 -28
- package/dist/js/internal/components/lightbox.mjs +2 -2
- package/dist/js/internal/components/lineChart.mjs +12 -0
- package/dist/js/internal/components/lineChart.mjs.map +7 -0
- package/dist/js/internal/components/mapView.mjs +9 -0
- package/dist/js/internal/components/mapView.mjs.map +7 -0
- package/dist/js/internal/components/menu.mjs +2 -2
- package/dist/js/internal/components/messageScroller.mjs +9 -0
- package/dist/js/internal/components/messageScroller.mjs.map +7 -0
- package/dist/js/internal/components/mixedChart.mjs +10 -0
- package/dist/js/internal/components/mixedChart.mjs.map +7 -0
- package/dist/js/internal/components/particles.mjs +8 -0
- package/dist/js/internal/components/particles.mjs.map +7 -0
- package/dist/js/internal/components/pieChart.mjs +9 -0
- package/dist/js/internal/components/pieChart.mjs.map +7 -0
- package/dist/js/internal/components/questionnaire.mjs +9 -0
- package/dist/js/internal/components/questionnaire.mjs.map +7 -0
- package/dist/js/internal/components/radarChart.mjs +9 -0
- package/dist/js/internal/components/radarChart.mjs.map +7 -0
- package/dist/js/internal/components/radialChart.mjs +9 -0
- package/dist/js/internal/components/radialChart.mjs.map +7 -0
- package/dist/js/internal/components/registry.mjs +35 -20
- package/dist/js/internal/components/richTextEditor.mjs +9 -0
- package/dist/js/internal/components/richTextEditor.mjs.map +7 -0
- package/dist/js/internal/components/scrollspy.mjs +1 -1
- package/dist/js/internal/components/select.mjs +3 -3
- package/dist/js/internal/components/slider.mjs +2 -2
- package/dist/js/internal/components/snackbar.mjs +2 -2
- package/dist/js/internal/components/tabs.mjs +3 -3
- package/dist/js/internal/components/timepicker.mjs +3 -3
- package/dist/js/internal/components/tooltip.mjs +2 -2
- package/dist/js/internal/core/utils.mjs +1 -1
- package/dist/js/internal/plugins/dockedDisplayPlugin.mjs +2 -2
- package/dist/js/modular.mjs +95 -35
- package/dist/types/behaviors/forms.d.ts +27 -0
- package/dist/types/components/barChart.d.ts +24 -0
- package/dist/types/components/carousel.d.ts +2 -0
- package/dist/types/components/columnChart.d.ts +21 -0
- package/dist/types/components/commandPalette.d.ts +96 -0
- package/dist/types/components/confetti.d.ts +103 -0
- package/dist/types/components/datepicker.d.ts +2 -0
- package/dist/types/components/heatmapChart.d.ts +49 -0
- package/dist/types/components/index.d.ts +15 -0
- package/dist/types/components/lineChart.d.ts +134 -0
- package/dist/types/components/mapView.d.ts +151 -0
- package/dist/types/components/menu.d.ts +31 -0
- package/dist/types/components/messageScroller.d.ts +111 -0
- package/dist/types/components/mixedChart.d.ts +25 -0
- package/dist/types/components/particles.d.ts +72 -0
- package/dist/types/components/pieChart.d.ts +43 -0
- package/dist/types/components/questionnaire.d.ts +96 -0
- package/dist/types/components/radarChart.d.ts +39 -0
- package/dist/types/components/radialChart.d.ts +45 -0
- package/dist/types/components/registry.d.ts +52 -0
- package/dist/types/components/richTextEditor.d.ts +71 -0
- package/dist/types/components/slider.d.ts +1 -1
- package/dist/types/components/snackbar.d.ts +2 -0
- package/dist/types/components/tabs.d.ts +1 -1
- package/dist/types/modular.d.ts +10 -1
- package/dist/types/plugins/dockedDisplayPlugin.d.ts +3 -3
- package/package.json +5 -1
- package/src/sass/README.md +3 -6
- package/src/sass/abstracts/_index.scss +1 -1
- package/src/sass/abstracts/_mixins.scss +226 -0
- package/src/sass/abstracts/_variables.scss +14 -0
- package/src/sass/base/_typography.scss +45 -0
- package/src/sass/components/_accordion.scss +161 -0
- package/src/sass/components/_aura.scss +119 -0
- package/src/sass/components/_avatar.scss +86 -0
- package/src/sass/components/_bar-chart.scss +285 -0
- package/src/sass/components/_bottom-sheet.scss +1 -0
- package/src/sass/components/_button-groups.scss +13 -3
- package/src/sass/components/_buttons.scss +1 -1
- package/src/sass/components/_cards.scss +16 -13
- package/src/sass/components/_column-chart.scss +134 -0
- package/src/sass/components/_command-palette.scss +140 -0
- package/src/sass/components/_confetti.scss +14 -0
- package/src/sass/components/_countdown.scss +89 -0
- package/src/sass/components/_data-table.scss +188 -0
- package/src/sass/components/_dialog.scss +1 -0
- package/src/sass/components/_empty-state.scss +78 -0
- package/src/sass/components/_expanding-card.scss +3 -0
- package/src/sass/components/_heatmap-chart.scss +156 -0
- package/src/sass/components/_index.scss +30 -0
- package/src/sass/components/_kpi.scss +162 -0
- package/src/sass/components/_line-chart.scss +102 -0
- package/src/sass/components/_map.scss +188 -0
- package/src/sass/components/_marquee.scss +111 -0
- package/src/sass/components/_menu.scss +10 -0
- package/src/sass/components/_message-scroller.scss +66 -0
- package/src/sass/components/_message.scss +202 -0
- package/src/sass/components/_mixed-chart.scss +157 -0
- package/src/sass/components/_navigation-bar.scss +1 -0
- package/src/sass/components/_navigation-rail.scss +9 -0
- package/src/sass/components/_particles.scss +14 -0
- package/src/sass/components/_pie-chart.scss +203 -0
- package/src/sass/components/_popover.scss +112 -0
- package/src/sass/components/_questionnaire.scss +144 -0
- package/src/sass/components/_radar-chart.scss +205 -0
- package/src/sass/components/_radial-chart.scss +174 -0
- package/src/sass/components/_rich-text-editor.scss +99 -0
- package/src/sass/components/_search.scss +1 -0
- package/src/sass/components/_side-sheet.scss +1 -0
- package/src/sass/components/_skeleton.scss +81 -0
- package/src/sass/components/_snackbar.scss +1 -0
- package/src/sass/components/_stat.scss +86 -0
- package/src/sass/components/_stepper.scss +190 -0
- package/src/sass/components/_table_of_contents.scss +3 -1
- package/src/sass/components/_timeline.scss +114 -0
- package/src/sass/components/_toolbar.scss +1 -0
- package/src/sass/components/_tooltip.scss +2 -0
- package/src/sass/components/_tree.scss +139 -0
- package/src/sass/components/forms/_checkboxes.scss +7 -2
- package/src/sass/components/forms/_drop-zone.scss +128 -0
- package/src/sass/components/forms/_index.scss +2 -0
- package/src/sass/components/forms/_input-fields.scss +133 -2
- package/src/sass/components/forms/_rating.scss +165 -0
- package/src/sass/tokens/_reference.scss +3 -15
- package/src/sass/utilities/_index.scss +2 -0
- package/src/sass/utilities/_scroll-area.scss +35 -0
- package/src/sass/utilities/_shapes.scss +164 -0
- package/dist/js/chunks/chunk-3VOK6J7U.mjs.map +0 -7
- package/dist/js/chunks/chunk-CKQP6P76.mjs.map +0 -7
- package/dist/js/chunks/chunk-EC63CHXU.mjs.map +0 -7
- package/dist/js/chunks/chunk-G3I5H2U3.mjs +0 -7
- package/dist/js/chunks/chunk-G3I5H2U3.mjs.map +0 -7
- package/dist/js/chunks/chunk-GZWPXXY6.mjs.map +0 -7
- package/dist/js/chunks/chunk-H2WA4XHH.mjs.map +0 -7
- package/dist/js/chunks/chunk-HLDXZ7XB.mjs.map +0 -7
- package/dist/js/chunks/chunk-HNFSMIBY.mjs.map +0 -7
- package/dist/js/chunks/chunk-LWBQIX5J.mjs.map +0 -7
- package/dist/js/chunks/chunk-NNYP33NB.mjs.map +0 -7
- package/dist/js/chunks/chunk-PSSHQWCX.mjs +0 -100
- package/dist/js/chunks/chunk-PSSHQWCX.mjs.map +0 -7
- package/dist/js/chunks/chunk-WJXDBPXQ.mjs.map +0 -7
- package/dist/js/chunks/chunk-WWPAPHMA.mjs +0 -1
- package/dist/js/chunks/chunk-YSBCHYXP.mjs.map +0 -7
- /package/dist/js/chunks/{chunk-HB6IW7QQ.mjs.map → chunk-3GVFZ4XO.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-DLWYKFEI.mjs.map → chunk-CUQPB25U.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-6WLSDZ4O.mjs.map → chunk-DAIU5SLG.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-RFZZCYVU.mjs.map → chunk-KY6U46SM.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-UBLRRJ7N.mjs.map → chunk-PM655FXL.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-IFXVDDDH.mjs.map → chunk-QERLECVU.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-UNEMYWEW.mjs.map → chunk-S3XDFGSZ.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-UAEAQBRC.mjs.map → chunk-X7OCHZVF.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-QVVEWBOF.mjs.map → chunk-Z2NRKI7R.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-PIJ2TAR3.mjs.map → chunk-ZAS25YFD.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-WWPAPHMA.mjs.map → chunk-ZKZKNSLC.mjs.map} +0 -0
package/dist/js/expressive.cjs
CHANGED
|
@@ -22,22 +22,37 @@ __export(index_exports, {
|
|
|
22
22
|
AppBar: () => AppBar,
|
|
23
23
|
AutoInit: () => AutoInit,
|
|
24
24
|
Autocomplete: () => Autocomplete,
|
|
25
|
+
BarChart: () => BarChart,
|
|
25
26
|
BottomSheets: () => BottomSheets,
|
|
26
27
|
ButtonGroup: () => ButtonGroup,
|
|
27
28
|
Cards: () => Cards,
|
|
28
29
|
Carousel: () => Carousel,
|
|
29
30
|
CharacterCounter: () => CharacterCounter,
|
|
30
31
|
Chips: () => Chips,
|
|
32
|
+
ColumnChart: () => ColumnChart,
|
|
33
|
+
CommandPalette: () => CommandPalette,
|
|
34
|
+
Confetti: () => Confetti,
|
|
31
35
|
Datepicker: () => Datepicker,
|
|
32
36
|
Dialogs: () => Dialogs,
|
|
33
37
|
ExpandingCard: () => ExpandingCard,
|
|
34
38
|
FloatingActionButton: () => FloatingActionButton,
|
|
35
39
|
FormSelect: () => FormSelect,
|
|
36
40
|
Forms: () => Forms,
|
|
41
|
+
HeatmapChart: () => HeatmapChart,
|
|
37
42
|
Lightbox: () => Lightbox,
|
|
43
|
+
LineChart: () => LineChart,
|
|
44
|
+
MapView: () => MapView,
|
|
38
45
|
Menu: () => Menu,
|
|
46
|
+
MessageScroller: () => MessageScroller,
|
|
47
|
+
MixedChart: () => MixedChart,
|
|
39
48
|
NavigationRail: () => NavigationRail,
|
|
49
|
+
Particles: () => Particles,
|
|
50
|
+
PieChart: () => PieChart,
|
|
51
|
+
Questionnaire: () => Questionnaire,
|
|
52
|
+
RadarChart: () => RadarChart,
|
|
53
|
+
RadialChart: () => RadialChart,
|
|
40
54
|
Range: () => Slider,
|
|
55
|
+
RichTextEditor: () => RichTextEditor,
|
|
41
56
|
ScrollSpy: () => ScrollSpy,
|
|
42
57
|
SideSheets: () => SideSheets,
|
|
43
58
|
Slider: () => Slider,
|
|
@@ -227,9 +242,9 @@ var _Utils = class _Utils {
|
|
|
227
242
|
static throttle(func, wait) {
|
|
228
243
|
let timeout = null;
|
|
229
244
|
let lastArgs = null;
|
|
230
|
-
let previous =
|
|
245
|
+
let previous = -Infinity;
|
|
231
246
|
return (...args) => {
|
|
232
|
-
const now =
|
|
247
|
+
const now = performance.now();
|
|
233
248
|
const remaining = wait - (now - previous);
|
|
234
249
|
if (remaining <= 0) {
|
|
235
250
|
clearTimeout(timeout);
|
|
@@ -240,7 +255,7 @@ var _Utils = class _Utils {
|
|
|
240
255
|
} else if (!timeout) {
|
|
241
256
|
lastArgs = args;
|
|
242
257
|
timeout = setTimeout(() => {
|
|
243
|
-
previous =
|
|
258
|
+
previous = performance.now();
|
|
244
259
|
timeout = null;
|
|
245
260
|
func(...lastArgs ?? []);
|
|
246
261
|
lastArgs = null;
|
|
@@ -772,6 +787,7 @@ var _defaults3 = {
|
|
|
772
787
|
coverTrigger: false,
|
|
773
788
|
closeOnClick: true,
|
|
774
789
|
hover: false,
|
|
790
|
+
contextMenu: false,
|
|
775
791
|
inDuration: 150,
|
|
776
792
|
outDuration: 250,
|
|
777
793
|
onOpenStart: null,
|
|
@@ -787,6 +803,40 @@ var _Menu = class _Menu extends Component {
|
|
|
787
803
|
this._pendingTimers = /* @__PURE__ */ new Set();
|
|
788
804
|
this._transition = 0;
|
|
789
805
|
this._menuClickTargets = /* @__PURE__ */ new WeakMap();
|
|
806
|
+
/**
|
|
807
|
+
* Viewport point the open context menu belongs to. Null outside a
|
|
808
|
+
* right-click, so a keyboard or programmatic open uses the region's corner
|
|
809
|
+
* rather than an old pointer position.
|
|
810
|
+
*/
|
|
811
|
+
this._contextPoint = null;
|
|
812
|
+
/** What had focus when a context menu opened; focus goes back there. */
|
|
813
|
+
this._contextReturn = null;
|
|
814
|
+
this._handleContextMenu = (e) => {
|
|
815
|
+
if (e.defaultPrevented) {
|
|
816
|
+
this.close();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const path = e.composedPath();
|
|
820
|
+
if (_Menu._menus.some((menu) => menu !== this && menu.isOpen && path.includes(menu.menuEl))) return;
|
|
821
|
+
e.preventDefault();
|
|
822
|
+
if (!this.isOpen) {
|
|
823
|
+
const root = this.el.getRootNode();
|
|
824
|
+
this._contextReturn = root.activeElement instanceof HTMLElement ? root.activeElement : null;
|
|
825
|
+
}
|
|
826
|
+
const rect = this.el.getBoundingClientRect();
|
|
827
|
+
const fromPointer = (e.clientX !== 0 || e.clientY !== 0) && e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
|
|
828
|
+
this._contextPoint = fromPointer ? { x: e.clientX, y: e.clientY } : null;
|
|
829
|
+
if (this.isOpen) this.recalculateDimensions();
|
|
830
|
+
else this.open();
|
|
831
|
+
};
|
|
832
|
+
this._handleDocumentContextMenu = (e) => {
|
|
833
|
+
const path = e.composedPath();
|
|
834
|
+
if (path.includes(this.menuEl)) {
|
|
835
|
+
e.preventDefault();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (!path.includes(this.el)) this.close();
|
|
839
|
+
};
|
|
790
840
|
this._handleClick = (e) => {
|
|
791
841
|
e.preventDefault();
|
|
792
842
|
if (this.isOpen) {
|
|
@@ -833,6 +883,7 @@ var _Menu = class _Menu extends Component {
|
|
|
833
883
|
this.isTouchMoving = false;
|
|
834
884
|
};
|
|
835
885
|
this._handleTriggerKeydown = (e) => {
|
|
886
|
+
if (this.options.contextMenu) return;
|
|
836
887
|
const opensMenu = e.key === SPACE || e.key === Utils.keys.ENTER || e.key === Utils.keys.ARROW_DOWN || e.key === Utils.keys.ARROW_UP;
|
|
837
888
|
if (opensMenu && !this.isOpen) {
|
|
838
889
|
e.preventDefault();
|
|
@@ -991,7 +1042,7 @@ var _Menu = class _Menu extends Component {
|
|
|
991
1042
|
this._placeMenu();
|
|
992
1043
|
this._schedule(() => this._setupTemporaryEventHandlers(), 0);
|
|
993
1044
|
this._animateIn();
|
|
994
|
-
this.
|
|
1045
|
+
this._setExpanded(true);
|
|
995
1046
|
};
|
|
996
1047
|
/**
|
|
997
1048
|
* Close menu.
|
|
@@ -1013,10 +1064,13 @@ var _Menu = class _Menu extends Component {
|
|
|
1013
1064
|
if (transition !== this._transition) return;
|
|
1014
1065
|
this._animateOut();
|
|
1015
1066
|
this._removeTemporaryEventHandlers();
|
|
1016
|
-
this.
|
|
1067
|
+
this._setExpanded(false);
|
|
1017
1068
|
if (this.options.autoFocus && this.menuEl.contains(focused) && (root.activeElement === focused || this.el.ownerDocument.activeElement === this.el.ownerDocument.body)) {
|
|
1018
|
-
this.
|
|
1069
|
+
const back = this._contextReturn;
|
|
1070
|
+
(back?.isConnected && back !== this.el.ownerDocument.body ? back : this.el).focus();
|
|
1019
1071
|
}
|
|
1072
|
+
this._contextReturn = null;
|
|
1073
|
+
this._contextPoint = null;
|
|
1020
1074
|
};
|
|
1021
1075
|
/**
|
|
1022
1076
|
* While menu is open, you can recalculate its dimensions if its contents have changed.
|
|
@@ -1034,6 +1088,7 @@ var _Menu = class _Menu extends Component {
|
|
|
1034
1088
|
this._originalInert = this.menuEl?.getAttribute("inert") ?? null;
|
|
1035
1089
|
this.options = {
|
|
1036
1090
|
..._Menu.defaults,
|
|
1091
|
+
contextMenu: el.hasAttribute("data-context-menu"),
|
|
1037
1092
|
...options
|
|
1038
1093
|
};
|
|
1039
1094
|
this.isOpen = false;
|
|
@@ -1041,10 +1096,12 @@ var _Menu = class _Menu extends Component {
|
|
|
1041
1096
|
this.isTouchMoving = false;
|
|
1042
1097
|
this.focusedIndex = -1;
|
|
1043
1098
|
this.filterQuery = [];
|
|
1044
|
-
this.
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
this.el.
|
|
1099
|
+
if (!this.options.contextMenu) {
|
|
1100
|
+
this.el.ariaExpanded = "false";
|
|
1101
|
+
if (!this.el.hasAttribute("aria-haspopup")) this.el.setAttribute("aria-haspopup", "menu");
|
|
1102
|
+
if (this.id && !this.el.hasAttribute("aria-controls"))
|
|
1103
|
+
this.el.setAttribute("aria-controls", this.id);
|
|
1104
|
+
}
|
|
1048
1105
|
this._moveMenuToElement();
|
|
1049
1106
|
this._setupAccessibility();
|
|
1050
1107
|
this._makeMenuFocusable();
|
|
@@ -1068,7 +1125,7 @@ var _Menu = class _Menu extends Component {
|
|
|
1068
1125
|
destroy() {
|
|
1069
1126
|
this._clearPendingTimers();
|
|
1070
1127
|
this.isOpen = false;
|
|
1071
|
-
this.
|
|
1128
|
+
this._setExpanded(false);
|
|
1072
1129
|
clearTimeout(this.filterTimeout);
|
|
1073
1130
|
this._resetMenuStyles();
|
|
1074
1131
|
this._restoreInert();
|
|
@@ -1084,7 +1141,9 @@ var _Menu = class _Menu extends Component {
|
|
|
1084
1141
|
if (this.menuEl?.querySelector(":scope > li > menu")) {
|
|
1085
1142
|
this.menuEl.addEventListener("mouseover", this._handleSubmenuAlign);
|
|
1086
1143
|
}
|
|
1087
|
-
if (this.options.
|
|
1144
|
+
if (this.options.contextMenu) {
|
|
1145
|
+
this.el.addEventListener("contextmenu", this._handleContextMenu);
|
|
1146
|
+
} else if (this.options.hover) {
|
|
1088
1147
|
this.el.addEventListener("mouseenter", this._handleMouseEnter);
|
|
1089
1148
|
this.el.addEventListener("mouseleave", this._handleMouseLeave);
|
|
1090
1149
|
this.menuEl.addEventListener("mouseleave", this._handleMouseLeave);
|
|
@@ -1096,7 +1155,9 @@ var _Menu = class _Menu extends Component {
|
|
|
1096
1155
|
this.el.removeEventListener("keydown", this._handleTriggerKeydown);
|
|
1097
1156
|
this.menuEl?.removeEventListener("click", this._handleMenuClick);
|
|
1098
1157
|
this.menuEl?.removeEventListener("mouseover", this._handleSubmenuAlign);
|
|
1099
|
-
if (this.options.
|
|
1158
|
+
if (this.options.contextMenu) {
|
|
1159
|
+
this.el.removeEventListener("contextmenu", this._handleContextMenu);
|
|
1160
|
+
} else if (this.options.hover) {
|
|
1100
1161
|
this.el.removeEventListener("mouseenter", this._handleMouseEnter);
|
|
1101
1162
|
this.el.removeEventListener("mouseleave", this._handleMouseLeave);
|
|
1102
1163
|
this.menuEl?.removeEventListener("mouseleave", this._handleMouseLeave);
|
|
@@ -1112,6 +1173,7 @@ var _Menu = class _Menu extends Component {
|
|
|
1112
1173
|
});
|
|
1113
1174
|
this.menuEl?.addEventListener("keydown", this._handleMenuKeydown);
|
|
1114
1175
|
window.addEventListener("resize", this._handleWindowResize);
|
|
1176
|
+
if (this.options.contextMenu) document.addEventListener("contextmenu", this._handleDocumentContextMenu);
|
|
1115
1177
|
}
|
|
1116
1178
|
_removeTemporaryEventHandlers() {
|
|
1117
1179
|
document.body.removeEventListener("click", this._handleDocumentClick);
|
|
@@ -1119,6 +1181,7 @@ var _Menu = class _Menu extends Component {
|
|
|
1119
1181
|
document.body.removeEventListener("touchmove", this._handleDocumentTouchmove);
|
|
1120
1182
|
this.menuEl?.removeEventListener("keydown", this._handleMenuKeydown);
|
|
1121
1183
|
window.removeEventListener("resize", this._handleWindowResize);
|
|
1184
|
+
document.removeEventListener("contextmenu", this._handleDocumentContextMenu);
|
|
1122
1185
|
}
|
|
1123
1186
|
_setupAccessibility() {
|
|
1124
1187
|
if (!this.menuEl) return;
|
|
@@ -1216,6 +1279,7 @@ var _Menu = class _Menu extends Component {
|
|
|
1216
1279
|
item.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
1217
1280
|
}
|
|
1218
1281
|
_resetMenuStyles() {
|
|
1282
|
+
if (!this.menuEl) return;
|
|
1219
1283
|
this.menuEl.style.display = "";
|
|
1220
1284
|
this._resetMenuPositioningStyles();
|
|
1221
1285
|
this.menuEl.style.transform = "";
|
|
@@ -1408,8 +1472,12 @@ var _Menu = class _Menu extends Component {
|
|
|
1408
1472
|
closestOverflowParent.style.position = "relative";
|
|
1409
1473
|
const natural = this.menuEl.getBoundingClientRect().width;
|
|
1410
1474
|
const triggerWidth = this.el.getBoundingClientRect().width;
|
|
1411
|
-
const idealWidth = this.options.constrainWidth && triggerWidth >= MIN_SURFACE_WIDTH ? triggerWidth : Math.min(MAX_SURFACE_WIDTH, Math.max(MIN_SURFACE_WIDTH, natural));
|
|
1475
|
+
const idealWidth = this.options.constrainWidth && !this.options.contextMenu && triggerWidth >= MIN_SURFACE_WIDTH ? triggerWidth : Math.min(MAX_SURFACE_WIDTH, Math.max(MIN_SURFACE_WIDTH, natural));
|
|
1412
1476
|
this.menuEl.style.width = idealWidth + "px";
|
|
1477
|
+
if (this.options.contextMenu) {
|
|
1478
|
+
this._placeMenuAtPoint(this._contextPoint ?? this._regionCorner(), closestOverflowParent);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1413
1481
|
const positionInfo = this._getMenuPosition(closestOverflowParent);
|
|
1414
1482
|
this.menuEl.style.left = positionInfo.x + "px";
|
|
1415
1483
|
this.menuEl.style.top = positionInfo.y + "px";
|
|
@@ -1417,6 +1485,58 @@ var _Menu = class _Menu extends Component {
|
|
|
1417
1485
|
this.menuEl.style.width = positionInfo.width + "px";
|
|
1418
1486
|
this.menuEl.style.transformOrigin = `${positionInfo.horizontalAlignment === "left" ? "0" : "100%"} ${positionInfo.verticalAlignment === "top" ? "0" : "100%"}`;
|
|
1419
1487
|
}
|
|
1488
|
+
/** Where a context menu opens without a pointer: the region's start corner. */
|
|
1489
|
+
_regionCorner() {
|
|
1490
|
+
const rect = this.el.getBoundingClientRect();
|
|
1491
|
+
const rtl = getComputedStyle(this.el).direction === "rtl";
|
|
1492
|
+
return { x: rtl ? rect.right : rect.left, y: rect.bottom };
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Places a context menu with its start corner on the point, flipping to
|
|
1496
|
+
* the other side of the point where it would not fit. The bounds are the
|
|
1497
|
+
* viewport, narrowed to a scrolling or clipping ancestor the menu sits in;
|
|
1498
|
+
* a menu taller than them gets their height and scrolls.
|
|
1499
|
+
*
|
|
1500
|
+
* The menu rests at left/top 0 of its containing block, so its rect is the
|
|
1501
|
+
* offset to subtract - the same trick _getMenuPosition uses. It is measured
|
|
1502
|
+
* without the entry animation's scale, which a second right-click can catch.
|
|
1503
|
+
*/
|
|
1504
|
+
_placeMenuAtPoint({ x, y }, container) {
|
|
1505
|
+
const { transform, transition } = this.menuEl.style;
|
|
1506
|
+
this.menuEl.style.transition = "none";
|
|
1507
|
+
this.menuEl.style.transform = "none";
|
|
1508
|
+
const menu = this.menuEl.getBoundingClientRect();
|
|
1509
|
+
this.menuEl.style.transform = transform;
|
|
1510
|
+
this.menuEl.style.transition = transition;
|
|
1511
|
+
let bounds = { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight };
|
|
1512
|
+
if (container && getComputedStyle(container).overflow !== "visible") {
|
|
1513
|
+
const clip = container.getBoundingClientRect();
|
|
1514
|
+
bounds = {
|
|
1515
|
+
left: Math.max(bounds.left, clip.left),
|
|
1516
|
+
top: Math.max(bounds.top, clip.top),
|
|
1517
|
+
right: Math.min(bounds.right, clip.right),
|
|
1518
|
+
bottom: Math.min(bounds.bottom, clip.bottom)
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
const width = menu.width;
|
|
1522
|
+
const height = Math.min(menu.height, bounds.bottom - bounds.top);
|
|
1523
|
+
this.isScrollable = height < menu.height;
|
|
1524
|
+
if (this.isScrollable) this.menuEl.style.height = height + "px";
|
|
1525
|
+
const rtl = getComputedStyle(this.el).direction === "rtl";
|
|
1526
|
+
const fitsRight = x + width <= bounds.right;
|
|
1527
|
+
const fitsLeft = x - width >= bounds.left;
|
|
1528
|
+
const leftward = rtl ? fitsLeft || !fitsRight : !fitsRight && fitsLeft;
|
|
1529
|
+
const upward = y + height > bounds.bottom && y - height >= bounds.top;
|
|
1530
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
|
|
1531
|
+
const left = clamp(leftward ? x - width : x, bounds.left, bounds.right - width);
|
|
1532
|
+
const top = clamp(upward ? y - height : y, bounds.top, bounds.bottom - height);
|
|
1533
|
+
this.menuEl.style.left = left - menu.left + "px";
|
|
1534
|
+
this.menuEl.style.top = top - menu.top + "px";
|
|
1535
|
+
this.menuEl.style.transformOrigin = `${leftward ? "100%" : "0"} ${upward ? "100%" : "0"}`;
|
|
1536
|
+
}
|
|
1537
|
+
_setExpanded(expanded) {
|
|
1538
|
+
if (!this.options.contextMenu) this.el.ariaExpanded = String(expanded);
|
|
1539
|
+
}
|
|
1420
1540
|
};
|
|
1421
1541
|
_Menu._menus = [];
|
|
1422
1542
|
var Menu = _Menu;
|
|
@@ -1471,8 +1591,9 @@ var _Autocomplete = class _Autocomplete extends Component {
|
|
|
1471
1591
|
this._handleInput = () => {
|
|
1472
1592
|
this.count = 0;
|
|
1473
1593
|
const actualValue = this.el.value.toLocaleLowerCase();
|
|
1474
|
-
if (this.oldVal !== actualValue
|
|
1475
|
-
this.
|
|
1594
|
+
if (this.oldVal !== actualValue) {
|
|
1595
|
+
this._resetCurrentElementPosition();
|
|
1596
|
+
if (this.el.matches(":focus-visible") && (!this.menu.isOpen || actualValue.length < this.options.minLength)) this.open();
|
|
1476
1597
|
}
|
|
1477
1598
|
this._inputChangeDetection(actualValue);
|
|
1478
1599
|
};
|
|
@@ -1782,11 +1903,24 @@ var _Autocomplete = class _Autocomplete extends Component {
|
|
|
1782
1903
|
item.appendChild(img);
|
|
1783
1904
|
}
|
|
1784
1905
|
const inputText = this.el.value.toLocaleLowerCase();
|
|
1785
|
-
const
|
|
1906
|
+
const label = (entry.text || entry.id).toString();
|
|
1907
|
+
const parts = this._highlightPartialText(inputText, label);
|
|
1786
1908
|
const div = document.createElement("div");
|
|
1787
1909
|
div.setAttribute("style", "line-height:1.2;font-weight:500;");
|
|
1788
1910
|
if (this.options.allowUnsafeHTML) {
|
|
1789
|
-
div.innerHTML =
|
|
1911
|
+
div.innerHTML = label;
|
|
1912
|
+
const walker = document.createTreeWalker(div, NodeFilter.SHOW_TEXT);
|
|
1913
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
1914
|
+
const [before, match] = this._highlightPartialText(inputText, node.data);
|
|
1915
|
+
if (!match) continue;
|
|
1916
|
+
const matched = node.splitText(before.length);
|
|
1917
|
+
matched.splitText(match.length);
|
|
1918
|
+
const highlight = document.createElement("span");
|
|
1919
|
+
highlight.classList.add("highlight");
|
|
1920
|
+
matched.replaceWith(highlight);
|
|
1921
|
+
highlight.append(matched);
|
|
1922
|
+
break;
|
|
1923
|
+
}
|
|
1790
1924
|
} else {
|
|
1791
1925
|
div.appendChild(document.createTextNode(parts[0]));
|
|
1792
1926
|
if (parts[1]) {
|
|
@@ -1878,7 +2012,7 @@ var _Autocomplete = class _Autocomplete extends Component {
|
|
|
1878
2012
|
);
|
|
1879
2013
|
}
|
|
1880
2014
|
if (this.options.isMultiSelect) {
|
|
1881
|
-
this._renderMenu();
|
|
2015
|
+
if (!open) this._renderMenu();
|
|
1882
2016
|
} else {
|
|
1883
2017
|
this._refreshInputText();
|
|
1884
2018
|
}
|
|
@@ -2276,12 +2410,10 @@ var Slider = class _Slider extends Component {
|
|
|
2276
2410
|
this._sync();
|
|
2277
2411
|
this.thumb.classList.add("active");
|
|
2278
2412
|
};
|
|
2279
|
-
this.
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
this.thumb.classList.add("active");
|
|
2284
|
-
}
|
|
2413
|
+
this._handleRangeInput = () => {
|
|
2414
|
+
this._clampDual();
|
|
2415
|
+
this._sync();
|
|
2416
|
+
this.thumb.classList.add("active");
|
|
2285
2417
|
};
|
|
2286
2418
|
this._handleRangePointerUp = (e) => {
|
|
2287
2419
|
if (!e.isPrimary) return;
|
|
@@ -2345,8 +2477,7 @@ var Slider = class _Slider extends Component {
|
|
|
2345
2477
|
_setupEventHandlers() {
|
|
2346
2478
|
this.el.addEventListener("change", this._handleRangeChange);
|
|
2347
2479
|
this.el.addEventListener("pointerdown", this._handleRangePointerDown);
|
|
2348
|
-
this.el.addEventListener("input", this.
|
|
2349
|
-
this.el.addEventListener("pointermove", this._handleRangeInputOrMove, { passive: true });
|
|
2480
|
+
this.el.addEventListener("input", this._handleRangeInput);
|
|
2350
2481
|
this.el.addEventListener("pointerup", this._handleRangePointerUp);
|
|
2351
2482
|
this.el.addEventListener("pointercancel", this._handleRangePointerUp);
|
|
2352
2483
|
this.el.addEventListener("blur", this._handleRangeRelease);
|
|
@@ -2355,8 +2486,7 @@ var Slider = class _Slider extends Component {
|
|
|
2355
2486
|
_removeEventHandlers() {
|
|
2356
2487
|
this.el.removeEventListener("change", this._handleRangeChange);
|
|
2357
2488
|
this.el.removeEventListener("pointerdown", this._handleRangePointerDown);
|
|
2358
|
-
this.el.removeEventListener("input", this.
|
|
2359
|
-
this.el.removeEventListener("pointermove", this._handleRangeInputOrMove);
|
|
2489
|
+
this.el.removeEventListener("input", this._handleRangeInput);
|
|
2360
2490
|
this.el.removeEventListener("pointerup", this._handleRangePointerUp);
|
|
2361
2491
|
this.el.removeEventListener("pointercancel", this._handleRangePointerUp);
|
|
2362
2492
|
this.el.removeEventListener("blur", this._handleRangeRelease);
|
|
@@ -2524,7 +2654,10 @@ var DISMISS_DISTANCE = 96;
|
|
|
2524
2654
|
var DISMISS_VELOCITY = 0.5;
|
|
2525
2655
|
var FLICK_TIMEOUT = 100;
|
|
2526
2656
|
var DRAG_SLOP = 4;
|
|
2657
|
+
var installed = /* @__PURE__ */ new Set();
|
|
2527
2658
|
function installSheetDrag(config) {
|
|
2659
|
+
if (installed.has(config.selector)) return;
|
|
2660
|
+
installed.add(config.selector);
|
|
2528
2661
|
const coord = (event) => config.axis === "x" ? event.clientX : event.clientY;
|
|
2529
2662
|
let observer;
|
|
2530
2663
|
let drag = null;
|
|
@@ -2685,7 +2818,7 @@ var SideSheets = class {
|
|
|
2685
2818
|
};
|
|
2686
2819
|
|
|
2687
2820
|
// src/ts/behaviors/forms.ts
|
|
2688
|
-
var
|
|
2821
|
+
var _Forms = class _Forms {
|
|
2689
2822
|
/**
|
|
2690
2823
|
* Checks if the label has validation and apply
|
|
2691
2824
|
* the correct class and styles
|
|
@@ -2727,10 +2860,20 @@ var Forms = class _Forms {
|
|
|
2727
2860
|
const contentHeight = textarea.scrollHeight + borders;
|
|
2728
2861
|
textarea.style.height = Math.max(originalHeight, contentHeight) + "px";
|
|
2729
2862
|
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Safe to call more than once: the root entry calls it on import, and a page
|
|
2865
|
+
* that also loads the modular entry may call it again.
|
|
2866
|
+
*/
|
|
2730
2867
|
static Init() {
|
|
2868
|
+
if (_Forms._initialized) return;
|
|
2869
|
+
_Forms._initialized = true;
|
|
2731
2870
|
Utils.onDocumentReady(() => {
|
|
2732
2871
|
document.addEventListener("change", (e) => {
|
|
2733
2872
|
const target = e.target;
|
|
2873
|
+
if (target instanceof HTMLInputElement && target.type === "file") {
|
|
2874
|
+
const zone = target.closest(".drop-zone");
|
|
2875
|
+
if (zone) _Forms.listDropZoneFiles(zone, target);
|
|
2876
|
+
}
|
|
2734
2877
|
if (target instanceof HTMLInputElement) {
|
|
2735
2878
|
_Forms.validateField(target);
|
|
2736
2879
|
}
|
|
@@ -2746,6 +2889,12 @@ var Forms = class _Forms {
|
|
|
2746
2889
|
}
|
|
2747
2890
|
}
|
|
2748
2891
|
});
|
|
2892
|
+
document.addEventListener("click", _Forms.togglePassword);
|
|
2893
|
+
document.addEventListener("submit", _Forms.resetFormExtras, true);
|
|
2894
|
+
document.addEventListener("reset", _Forms.resetFormExtras, true);
|
|
2895
|
+
for (const type of ["dragenter", "dragover", "dragleave", "drop"]) {
|
|
2896
|
+
document.addEventListener(type, _Forms.markDropZone);
|
|
2897
|
+
}
|
|
2749
2898
|
document.querySelectorAll(".expressive-textarea").forEach((textArea) => {
|
|
2750
2899
|
_Forms.InitTextarea(textArea);
|
|
2751
2900
|
});
|
|
@@ -2754,6 +2903,75 @@ var Forms = class _Forms {
|
|
|
2754
2903
|
});
|
|
2755
2904
|
});
|
|
2756
2905
|
}
|
|
2906
|
+
/**
|
|
2907
|
+
* A .password-toggle button shows or hides the password in its field. The
|
|
2908
|
+
* button's own name stays put ("Show password"); aria-pressed carries the
|
|
2909
|
+
* state, so there is no generated text to translate.
|
|
2910
|
+
*/
|
|
2911
|
+
static togglePassword(e) {
|
|
2912
|
+
const button = e.target.closest?.(".password-toggle");
|
|
2913
|
+
const input = button?.closest(".field")?.querySelector(
|
|
2914
|
+
'input[type="password"], input[data-password]'
|
|
2915
|
+
);
|
|
2916
|
+
if (!input) return;
|
|
2917
|
+
const show = input.type === "password";
|
|
2918
|
+
input.toggleAttribute("data-password", show);
|
|
2919
|
+
input.type = show ? "text" : "password";
|
|
2920
|
+
button.setAttribute("aria-pressed", String(show));
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* A revealed password goes back to type="password" when its form is
|
|
2924
|
+
* submitted or reset, so it is never sent or autofilled as plain text and
|
|
2925
|
+
* a reset field does not stay revealed. A reset form also drops the file
|
|
2926
|
+
* lists it would otherwise leave showing: reset fires no change event.
|
|
2927
|
+
*/
|
|
2928
|
+
static resetFormExtras(e) {
|
|
2929
|
+
const form = e.target;
|
|
2930
|
+
if (!(form instanceof HTMLFormElement)) return;
|
|
2931
|
+
form.querySelectorAll("input[data-password]").forEach((input) => {
|
|
2932
|
+
input.removeAttribute("data-password");
|
|
2933
|
+
input.type = "password";
|
|
2934
|
+
input.closest(".field")?.querySelector(".password-toggle")?.setAttribute("aria-pressed", "false");
|
|
2935
|
+
});
|
|
2936
|
+
if (e.type === "reset") {
|
|
2937
|
+
form.querySelectorAll(".drop-zone-files").forEach((list) => list.replaceChildren());
|
|
2938
|
+
form.querySelectorAll(".drop-zone.dragover").forEach((zone) => zone.classList.remove("dragover"));
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* The native file input covers its .drop-zone, so the browser already takes
|
|
2943
|
+
* a dropped file. This only shows the drag: the input is the topmost
|
|
2944
|
+
* element, so every drag event over the zone targets it and leaving it is
|
|
2945
|
+
* leaving the zone.
|
|
2946
|
+
*/
|
|
2947
|
+
static markDropZone(e) {
|
|
2948
|
+
const input = e.target;
|
|
2949
|
+
if (!(input instanceof HTMLInputElement) || input.type !== "file") return;
|
|
2950
|
+
const zone = input.closest(".drop-zone");
|
|
2951
|
+
if (!zone) return;
|
|
2952
|
+
const over = e.type === "dragenter" || e.type === "dragover";
|
|
2953
|
+
zone.classList.toggle("dragover", over && !input.disabled);
|
|
2954
|
+
}
|
|
2955
|
+
/** Lists the chosen files, name and size, in the zone's .drop-zone-files. */
|
|
2956
|
+
static listDropZoneFiles(zone, input) {
|
|
2957
|
+
const list = zone.querySelector(".drop-zone-files");
|
|
2958
|
+
if (!list) return;
|
|
2959
|
+
const size = (bytes) => {
|
|
2960
|
+
const [value, unit] = bytes >= 1e6 ? [bytes / 1e6, "megabyte"] : bytes >= 1e3 ? [bytes / 1e3, "kilobyte"] : [bytes, "byte"];
|
|
2961
|
+
return new Intl.NumberFormat(void 0, { style: "unit", unit, maximumFractionDigits: 1 }).format(value);
|
|
2962
|
+
};
|
|
2963
|
+
list.replaceChildren(
|
|
2964
|
+
...Array.from(input.files ?? [], (file) => {
|
|
2965
|
+
const item = document.createElement("li");
|
|
2966
|
+
const name = document.createElement("span");
|
|
2967
|
+
const meta = document.createElement("span");
|
|
2968
|
+
name.textContent = file.name;
|
|
2969
|
+
meta.textContent = size(file.size);
|
|
2970
|
+
item.append(name, meta);
|
|
2971
|
+
return item;
|
|
2972
|
+
})
|
|
2973
|
+
);
|
|
2974
|
+
}
|
|
2757
2975
|
static InitTextarea(textarea) {
|
|
2758
2976
|
textarea.setAttribute("original-height", textarea.getBoundingClientRect().height.toString());
|
|
2759
2977
|
_Forms.textareaAutoResize(textarea);
|
|
@@ -2775,6 +2993,8 @@ var Forms = class _Forms {
|
|
|
2775
2993
|
});
|
|
2776
2994
|
}
|
|
2777
2995
|
};
|
|
2996
|
+
_Forms._initialized = false;
|
|
2997
|
+
var Forms = _Forms;
|
|
2778
2998
|
|
|
2779
2999
|
// src/ts/components/appBar.ts
|
|
2780
3000
|
var _defaults7 = {};
|
|
@@ -3267,6 +3487,7 @@ var _Carousel = class _Carousel extends Component {
|
|
|
3267
3487
|
this._dragStartX = e.clientX;
|
|
3268
3488
|
this._dragStartY = e.clientY;
|
|
3269
3489
|
this._dragStartScroll = this._vertical ? this._scroller.scrollTop : this._scroller.scrollLeft;
|
|
3490
|
+
this._setDragListeners(true);
|
|
3270
3491
|
};
|
|
3271
3492
|
this._handleTrackPointerMove = (e) => {
|
|
3272
3493
|
if (!this.pressed || e.pointerId !== this._dragPointerId) return;
|
|
@@ -3291,6 +3512,7 @@ var _Carousel = class _Carousel extends Component {
|
|
|
3291
3512
|
if (!this.pressed || e.pointerId !== this._dragPointerId) return;
|
|
3292
3513
|
this.pressed = false;
|
|
3293
3514
|
this._dragPointerId = null;
|
|
3515
|
+
this._setDragListeners(false);
|
|
3294
3516
|
this.el.classList.remove("dragging");
|
|
3295
3517
|
try {
|
|
3296
3518
|
this._scroller.releasePointerCapture(e.pointerId);
|
|
@@ -3357,9 +3579,8 @@ var _Carousel = class _Carousel extends Component {
|
|
|
3357
3579
|
if (this._trackRaf !== null) return;
|
|
3358
3580
|
this._trackRaf = requestAnimationFrame(() => {
|
|
3359
3581
|
this._trackRaf = null;
|
|
3582
|
+
const index = this._ignoreScroll ? this.center : this._nearestIndex();
|
|
3360
3583
|
this._updateParallax();
|
|
3361
|
-
if (this._ignoreScroll) return;
|
|
3362
|
-
const index = this._nearestIndex();
|
|
3363
3584
|
if (index !== this.center) this._syncActive(index, this.dragged);
|
|
3364
3585
|
});
|
|
3365
3586
|
};
|
|
@@ -3372,6 +3593,7 @@ var _Carousel = class _Carousel extends Component {
|
|
|
3372
3593
|
};
|
|
3373
3594
|
this._finishScroll = () => {
|
|
3374
3595
|
this._ignoreScroll = false;
|
|
3596
|
+
this._scroller.style.scrollSnapType = "";
|
|
3375
3597
|
clearTimeout(this._scrollEndTimeout);
|
|
3376
3598
|
this._scroller.removeEventListener("scrollend", this._handleScrollEnd);
|
|
3377
3599
|
};
|
|
@@ -3551,9 +3773,6 @@ var _Carousel = class _Carousel extends Component {
|
|
|
3551
3773
|
});
|
|
3552
3774
|
this._scroller.addEventListener("pointerdown", this._handleTrackPointerDown);
|
|
3553
3775
|
this._scroller.addEventListener("wheel", this._handleNativeScroll, { passive: true });
|
|
3554
|
-
document.addEventListener("pointermove", this._handleTrackPointerMove);
|
|
3555
|
-
document.addEventListener("pointerup", this._handleTrackPointerUp);
|
|
3556
|
-
document.addEventListener("pointercancel", this._handleTrackPointerUp);
|
|
3557
3776
|
this._scroller.addEventListener("click", this._handleTrackClick, true);
|
|
3558
3777
|
this._scroller.addEventListener("dragstart", this._handleTrackDragStart);
|
|
3559
3778
|
this._scroller.addEventListener("transitionend", this._handleItemResize);
|
|
@@ -3572,9 +3791,7 @@ var _Carousel = class _Carousel extends Component {
|
|
|
3572
3791
|
this._scroller.removeEventListener("scroll", this._handleTrackScroll);
|
|
3573
3792
|
this._scroller.removeEventListener("pointerdown", this._handleTrackPointerDown);
|
|
3574
3793
|
this._scroller.removeEventListener("wheel", this._handleNativeScroll);
|
|
3575
|
-
|
|
3576
|
-
document.removeEventListener("pointerup", this._handleTrackPointerUp);
|
|
3577
|
-
document.removeEventListener("pointercancel", this._handleTrackPointerUp);
|
|
3794
|
+
this._setDragListeners(false);
|
|
3578
3795
|
this._scroller.removeEventListener("click", this._handleTrackClick, true);
|
|
3579
3796
|
this._scroller.removeEventListener("dragstart", this._handleTrackDragStart);
|
|
3580
3797
|
this._scroller.removeEventListener("transitionend", this._handleItemResize);
|
|
@@ -3641,6 +3858,13 @@ var _Carousel = class _Carousel extends Component {
|
|
|
3641
3858
|
this._autoAdvanceTimer = null;
|
|
3642
3859
|
}
|
|
3643
3860
|
}
|
|
3861
|
+
/** The document only hears from a carousel while a mouse press is down on its track. */
|
|
3862
|
+
_setDragListeners(on) {
|
|
3863
|
+
const method = on ? "addEventListener" : "removeEventListener";
|
|
3864
|
+
document[method]("pointermove", this._handleTrackPointerMove);
|
|
3865
|
+
document[method]("pointerup", this._handleTrackPointerUp);
|
|
3866
|
+
document[method]("pointercancel", this._handleTrackPointerUp);
|
|
3867
|
+
}
|
|
3644
3868
|
_paintIndicators(center) {
|
|
3645
3869
|
if (!this.showIndicators || !this._indicators) return;
|
|
3646
3870
|
const pos = (center % this.count + this.count) % this.count;
|
|
@@ -3667,40 +3891,31 @@ var _Carousel = class _Carousel extends Component {
|
|
|
3667
3891
|
}
|
|
3668
3892
|
}
|
|
3669
3893
|
_syncLayoutRoles(index) {
|
|
3670
|
-
const
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
this._generatedItemSizes.add(item);
|
|
3894
|
+
const sizes = this.images.map(() => "large");
|
|
3895
|
+
const setSize = (i, size) => {
|
|
3896
|
+
if (this.images[i]) sizes[i] = size;
|
|
3674
3897
|
};
|
|
3675
|
-
this.images.forEach((item) => setSize(item, "large"));
|
|
3676
3898
|
if (this.el.classList.contains("flat") || this.el.classList.contains("uncontained") || this.el.classList.contains("full-screen") && this._vertical) {
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
const item = this.images[index + offset];
|
|
3698
|
-
if (item) setSize(item, "large");
|
|
3699
|
-
}
|
|
3700
|
-
const medium = this.images[index + largeCount];
|
|
3701
|
-
const small = this.images[index + largeCount + 1];
|
|
3702
|
-
if (medium) setSize(medium, "medium");
|
|
3703
|
-
if (small) setSize(small, "small");
|
|
3899
|
+
} else if (this.el.classList.contains("hero") || this.el.classList.contains("full-screen-horizontal")) {
|
|
3900
|
+
setSize(index + 1, "small");
|
|
3901
|
+
if (this.el.classList.contains("center-aligned")) setSize(index - 1, "small");
|
|
3902
|
+
} else {
|
|
3903
|
+
const width = this.el.clientWidth;
|
|
3904
|
+
const desiredLargeCount = width >= 1600 ? 4 : width >= 1200 ? 3 : width >= 600 ? 2 : 1;
|
|
3905
|
+
const largeCount = Math.min(desiredLargeCount, Math.max(1, this.count - 2));
|
|
3906
|
+
this.el.classList.remove("multi-wide", "multi-large", "multi-extra-large");
|
|
3907
|
+
if (largeCount === 2) this.el.classList.add("multi-wide");
|
|
3908
|
+
else if (largeCount === 3) this.el.classList.add("multi-large");
|
|
3909
|
+
else if (largeCount >= 4) this.el.classList.add("multi-extra-large");
|
|
3910
|
+
this._generatedWideLayout = this._generatedWideLayout || largeCount > 1;
|
|
3911
|
+
setSize(index + largeCount, "medium");
|
|
3912
|
+
setSize(index + largeCount + 1, "small");
|
|
3913
|
+
}
|
|
3914
|
+
this.images.forEach((item, i) => {
|
|
3915
|
+
if (this._authoredItemSizes.has(item)) return;
|
|
3916
|
+
item.setAttribute("data-carousel-size", sizes[i]);
|
|
3917
|
+
this._generatedItemSizes.add(item);
|
|
3918
|
+
});
|
|
3704
3919
|
}
|
|
3705
3920
|
_syncAdaptiveMode() {
|
|
3706
3921
|
if (!this.el.classList.contains("full-screen")) return;
|
|
@@ -3725,7 +3940,7 @@ var _Carousel = class _Carousel extends Component {
|
|
|
3725
3940
|
(focusable ?? item).focus({ preventScroll: true });
|
|
3726
3941
|
}
|
|
3727
3942
|
_updateParallax() {
|
|
3728
|
-
if (
|
|
3943
|
+
if (this._motion.matches) {
|
|
3729
3944
|
this.images.forEach(
|
|
3730
3945
|
(item) => item.style.setProperty("--md-comp-carousel-item-parallax", "0px")
|
|
3731
3946
|
);
|
|
@@ -3734,8 +3949,9 @@ var _Carousel = class _Carousel extends Component {
|
|
|
3734
3949
|
const scrollerRect = this._scroller.getBoundingClientRect();
|
|
3735
3950
|
const viewportCenter = this._vertical ? scrollerRect.top + scrollerRect.height / 2 : scrollerRect.left + scrollerRect.width / 2;
|
|
3736
3951
|
const viewportSize = Math.max(1, this._vertical ? scrollerRect.height : scrollerRect.width);
|
|
3737
|
-
this.images.
|
|
3738
|
-
|
|
3952
|
+
const rects = this.images.map((item) => item.getBoundingClientRect());
|
|
3953
|
+
this.images.forEach((item, i) => {
|
|
3954
|
+
const rect = rects[i];
|
|
3739
3955
|
const itemCenter = this._vertical ? rect.top + rect.height / 2 : rect.left + rect.width / 2;
|
|
3740
3956
|
const progress = Math.max(-1, Math.min(1, (itemCenter - viewportCenter) / viewportSize));
|
|
3741
3957
|
item.style.setProperty("--md-comp-carousel-item-parallax", `${progress * -24}px`);
|
|
@@ -3788,6 +4004,7 @@ var _Carousel = class _Carousel extends Component {
|
|
|
3788
4004
|
const left = centered ? el.offsetLeft - (this._scroller.clientWidth - el.offsetWidth) / 2 : style.direction === "rtl" ? el.offsetLeft + el.offsetWidth - this._scroller.clientWidth + inlinePadding : el.offsetLeft - inlinePadding;
|
|
3789
4005
|
const top = el.offsetTop - blockPadding;
|
|
3790
4006
|
this._ignoreScroll = true;
|
|
4007
|
+
this._scroller.style.scrollSnapType = "none";
|
|
3791
4008
|
this._scroller.scrollTo({
|
|
3792
4009
|
left: this._vertical ? 0 : left,
|
|
3793
4010
|
top: this._vertical ? top : 0,
|
|
@@ -3865,60 +4082,1988 @@ var CharacterCounter = class _CharacterCounter extends Component {
|
|
|
3865
4082
|
this._setupCounter();
|
|
3866
4083
|
this._setupEventHandlers();
|
|
3867
4084
|
}
|
|
3868
|
-
static get defaults() {
|
|
3869
|
-
return _defaults11;
|
|
4085
|
+
static get defaults() {
|
|
4086
|
+
return _defaults11;
|
|
4087
|
+
}
|
|
4088
|
+
/**
|
|
4089
|
+
* Initializes instances of CharacterCounter.
|
|
4090
|
+
* @param els HTML elements.
|
|
4091
|
+
* @param options Component options.
|
|
4092
|
+
*/
|
|
4093
|
+
static init(els, options = {}) {
|
|
4094
|
+
return super.init(els, options, _CharacterCounter);
|
|
4095
|
+
}
|
|
4096
|
+
static getInstance(el) {
|
|
4097
|
+
return el["Expressive_CharacterCounter"];
|
|
4098
|
+
}
|
|
4099
|
+
destroy() {
|
|
4100
|
+
this._removeEventHandlers();
|
|
4101
|
+
this.el["Expressive_CharacterCounter"] = void 0;
|
|
4102
|
+
this._removeCounter();
|
|
4103
|
+
}
|
|
4104
|
+
_setupEventHandlers() {
|
|
4105
|
+
this.el.addEventListener("focus", this.updateCounter, true);
|
|
4106
|
+
this.el.addEventListener("input", this.updateCounter, true);
|
|
4107
|
+
}
|
|
4108
|
+
_removeEventHandlers() {
|
|
4109
|
+
this.el.removeEventListener("focus", this.updateCounter, true);
|
|
4110
|
+
this.el.removeEventListener("input", this.updateCounter, true);
|
|
4111
|
+
}
|
|
4112
|
+
_setupCounter() {
|
|
4113
|
+
this.counterEl = document.createElement("span");
|
|
4114
|
+
this.counterEl.classList.add("character-counter");
|
|
4115
|
+
this.counterEl.setAttribute("aria-live", "polite");
|
|
4116
|
+
this.counterEl.setAttribute("aria-atomic", "true");
|
|
4117
|
+
this.counterEl.style.float = "right";
|
|
4118
|
+
this.counterEl.style.fontSize = "12px";
|
|
4119
|
+
this.counterEl.style.height = "1";
|
|
4120
|
+
this.el.parentElement.appendChild(this.counterEl);
|
|
4121
|
+
}
|
|
4122
|
+
_removeCounter() {
|
|
4123
|
+
this.counterEl.remove();
|
|
4124
|
+
}
|
|
4125
|
+
_validateInput() {
|
|
4126
|
+
if (this.isValidLength && this.isInvalid) {
|
|
4127
|
+
this.isInvalid = false;
|
|
4128
|
+
this.el.classList.remove("invalid");
|
|
4129
|
+
} else if (!this.isValidLength && !this.isInvalid) {
|
|
4130
|
+
this.isInvalid = true;
|
|
4131
|
+
this.el.classList.remove("valid");
|
|
4132
|
+
this.el.classList.add("invalid");
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
};
|
|
4136
|
+
|
|
4137
|
+
// src/ts/components/confetti.ts
|
|
4138
|
+
var _defaults12 = {
|
|
4139
|
+
particleCount: 50,
|
|
4140
|
+
angle: 90,
|
|
4141
|
+
spread: 45,
|
|
4142
|
+
startVelocity: 45,
|
|
4143
|
+
decay: 0.9,
|
|
4144
|
+
gravity: 1,
|
|
4145
|
+
drift: 0,
|
|
4146
|
+
ticks: 200,
|
|
4147
|
+
origin: { x: 0.5, y: 0.5 },
|
|
4148
|
+
colors: [],
|
|
4149
|
+
shapes: ["square", "circle"],
|
|
4150
|
+
scalar: 1,
|
|
4151
|
+
manualStart: false
|
|
4152
|
+
};
|
|
4153
|
+
var THEME_COLORS = ["primary", "secondary", "tertiary", "primary-container", "secondary-container", "tertiary-container"];
|
|
4154
|
+
var FALLBACK_COLORS = ["#26ccff", "#a25afd", "#ff5e7e", "#88ff5a", "#fcff42", "#ffa62d", "#ff36ff"];
|
|
4155
|
+
var RAD = Math.PI / 180;
|
|
4156
|
+
var scenes = /* @__PURE__ */ new Map();
|
|
4157
|
+
var overlay = null;
|
|
4158
|
+
var Confetti = class _Confetti extends Component {
|
|
4159
|
+
constructor(el, options) {
|
|
4160
|
+
super(el, options, _Confetti);
|
|
4161
|
+
this._onClick = () => {
|
|
4162
|
+
this.fire();
|
|
4163
|
+
};
|
|
4164
|
+
this.el["Expressive_Confetti"] = this;
|
|
4165
|
+
this.options = { ..._Confetti.defaults, ...options };
|
|
4166
|
+
if (!(el instanceof HTMLCanvasElement)) el.addEventListener("click", this._onClick);
|
|
4167
|
+
else if (!this.options.manualStart) this.fire();
|
|
4168
|
+
}
|
|
4169
|
+
static get defaults() {
|
|
4170
|
+
return _defaults12;
|
|
4171
|
+
}
|
|
4172
|
+
static init(els, options = {}) {
|
|
4173
|
+
return super.init(els, options, _Confetti);
|
|
4174
|
+
}
|
|
4175
|
+
static getInstance(el) {
|
|
4176
|
+
return el["Expressive_Confetti"];
|
|
4177
|
+
}
|
|
4178
|
+
/**
|
|
4179
|
+
* Bursts over the whole viewport. Resolves when the last piece is gone.
|
|
4180
|
+
*/
|
|
4181
|
+
static fire(options = {}) {
|
|
4182
|
+
return burst(viewportCanvas, document.body, { ..._Confetti.defaults, ...options });
|
|
4183
|
+
}
|
|
4184
|
+
/**
|
|
4185
|
+
* Bursts on this canvas, or from this element's center over the viewport.
|
|
4186
|
+
* Resolves when the last piece is gone.
|
|
4187
|
+
*/
|
|
4188
|
+
fire(options = {}) {
|
|
4189
|
+
const merged = { ...this.options, ...options };
|
|
4190
|
+
if (this.el instanceof HTMLCanvasElement) {
|
|
4191
|
+
const canvas = this.el;
|
|
4192
|
+
return burst(() => canvas, canvas.parentElement ?? document.body, merged);
|
|
4193
|
+
}
|
|
4194
|
+
const r = this.el.getBoundingClientRect();
|
|
4195
|
+
merged.origin = { x: (r.left + r.width / 2) / innerWidth, y: (r.top + r.height / 2) / innerHeight };
|
|
4196
|
+
return burst(viewportCanvas, this.el.parentElement ?? document.body, merged);
|
|
4197
|
+
}
|
|
4198
|
+
destroy() {
|
|
4199
|
+
this.el.removeEventListener("click", this._onClick);
|
|
4200
|
+
if (this.el instanceof HTMLCanvasElement) stop(this.el);
|
|
4201
|
+
this.el["Expressive_Confetti"] = void 0;
|
|
4202
|
+
}
|
|
4203
|
+
};
|
|
4204
|
+
function burst(getCanvas, themed, o) {
|
|
4205
|
+
if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return Promise.resolve();
|
|
4206
|
+
const canvas = getCanvas();
|
|
4207
|
+
let scene = scenes.get(canvas);
|
|
4208
|
+
if (!scene) {
|
|
4209
|
+
const ctx = canvas.getContext("2d");
|
|
4210
|
+
if (!ctx) return Promise.resolve();
|
|
4211
|
+
scene = { ctx, pieces: [], frame: 0, last: 0, done: [], overlay: canvas === overlay };
|
|
4212
|
+
scenes.set(canvas, scene);
|
|
4213
|
+
}
|
|
4214
|
+
const { width, height } = fit(canvas);
|
|
4215
|
+
const colors = o.colors.length ? o.colors : themeColors(themed);
|
|
4216
|
+
const x = (o.origin.x ?? 0.5) * width;
|
|
4217
|
+
const y = (o.origin.y ?? 0.5) * height;
|
|
4218
|
+
for (let i = 0; i < o.particleCount; i++) {
|
|
4219
|
+
scene.pieces.push({
|
|
4220
|
+
x,
|
|
4221
|
+
y,
|
|
4222
|
+
velocity: o.startVelocity * (0.5 + Math.random()),
|
|
4223
|
+
angle: -o.angle * RAD + (0.5 - Math.random()) * o.spread * RAD,
|
|
4224
|
+
wobble: Math.random() * 10,
|
|
4225
|
+
wobbleSpeed: 0.05 + Math.random() * 0.05,
|
|
4226
|
+
tilt: (0.25 + Math.random() / 2) * Math.PI,
|
|
4227
|
+
tick: 0,
|
|
4228
|
+
color: colors[i % colors.length],
|
|
4229
|
+
shape: o.shapes[Math.floor(Math.random() * o.shapes.length)] ?? "square",
|
|
4230
|
+
options: o
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
4233
|
+
const s = scene;
|
|
4234
|
+
if (!s.frame) s.frame = requestAnimationFrame((now) => {
|
|
4235
|
+
s.last = now;
|
|
4236
|
+
draw(canvas, s, now);
|
|
4237
|
+
});
|
|
4238
|
+
return new Promise((resolve) => s.done.push(resolve));
|
|
4239
|
+
}
|
|
4240
|
+
function draw(canvas, scene, now) {
|
|
4241
|
+
const step = Math.min((now - scene.last) / (1e3 / 60), 4);
|
|
4242
|
+
scene.last = now;
|
|
4243
|
+
const { width, height } = fit(canvas);
|
|
4244
|
+
const { ctx } = scene;
|
|
4245
|
+
const dpr = canvas.width / (width || 1);
|
|
4246
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4247
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
4248
|
+
scene.pieces = scene.pieces.filter((p) => {
|
|
4249
|
+
const o = p.options;
|
|
4250
|
+
p.x += (Math.cos(p.angle) * p.velocity + o.drift) * step;
|
|
4251
|
+
p.y += (Math.sin(p.angle) * p.velocity + o.gravity * 3) * step;
|
|
4252
|
+
p.velocity *= o.decay ** step;
|
|
4253
|
+
p.wobble += p.wobbleSpeed * step;
|
|
4254
|
+
p.tilt += 0.1 * step;
|
|
4255
|
+
p.tick += step;
|
|
4256
|
+
if (p.tick >= o.ticks) return false;
|
|
4257
|
+
const size = 8 * o.scalar;
|
|
4258
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
4259
|
+
ctx.translate(p.x + 10 * o.scalar * Math.cos(p.wobble), p.y + 10 * o.scalar * Math.sin(p.wobble));
|
|
4260
|
+
ctx.rotate(p.tilt);
|
|
4261
|
+
ctx.scale(1, Math.cos(p.wobble));
|
|
4262
|
+
ctx.globalAlpha = 1 - p.tick / o.ticks;
|
|
4263
|
+
ctx.fillStyle = p.color;
|
|
4264
|
+
ctx.beginPath();
|
|
4265
|
+
if (p.shape === "circle") ctx.arc(0, 0, size / 2, 0, 2 * Math.PI);
|
|
4266
|
+
else if (p.shape === "star") star(ctx, size * 0.75);
|
|
4267
|
+
else ctx.rect(-size / 2, -size / 2, size, size);
|
|
4268
|
+
ctx.fill();
|
|
4269
|
+
return true;
|
|
4270
|
+
});
|
|
4271
|
+
if (scene.pieces.length) scene.frame = requestAnimationFrame((t) => draw(canvas, scene, t));
|
|
4272
|
+
else stop(canvas);
|
|
4273
|
+
}
|
|
4274
|
+
function stop(canvas) {
|
|
4275
|
+
const scene = scenes.get(canvas);
|
|
4276
|
+
if (!scene) return;
|
|
4277
|
+
cancelAnimationFrame(scene.frame);
|
|
4278
|
+
scenes.delete(canvas);
|
|
4279
|
+
scene.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4280
|
+
scene.ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
4281
|
+
if (scene.overlay) {
|
|
4282
|
+
canvas.remove();
|
|
4283
|
+
overlay = null;
|
|
4284
|
+
}
|
|
4285
|
+
for (const resolve of scene.done) resolve();
|
|
4286
|
+
}
|
|
4287
|
+
function star(ctx, r) {
|
|
4288
|
+
for (let i = 0; i < 10; i++) {
|
|
4289
|
+
const a = i * Math.PI / 5 - Math.PI / 2;
|
|
4290
|
+
const d = i % 2 ? r / 2 : r;
|
|
4291
|
+
ctx.lineTo(Math.cos(a) * d, Math.sin(a) * d);
|
|
4292
|
+
}
|
|
4293
|
+
ctx.closePath();
|
|
4294
|
+
}
|
|
4295
|
+
function fit(canvas) {
|
|
4296
|
+
const width = canvas.clientWidth;
|
|
4297
|
+
const height = canvas.clientHeight;
|
|
4298
|
+
const w = Math.round(width * devicePixelRatio);
|
|
4299
|
+
const h = Math.round(height * devicePixelRatio);
|
|
4300
|
+
if (canvas.width !== w || canvas.height !== h) {
|
|
4301
|
+
canvas.width = w;
|
|
4302
|
+
canvas.height = h;
|
|
4303
|
+
}
|
|
4304
|
+
return { width, height };
|
|
4305
|
+
}
|
|
4306
|
+
function viewportCanvas() {
|
|
4307
|
+
if (overlay?.isConnected) return overlay;
|
|
4308
|
+
overlay = document.createElement("canvas");
|
|
4309
|
+
overlay.className = "confetti-overlay";
|
|
4310
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
4311
|
+
Object.assign(overlay.style, {
|
|
4312
|
+
position: "fixed",
|
|
4313
|
+
inset: "0",
|
|
4314
|
+
width: "100%",
|
|
4315
|
+
height: "100%",
|
|
4316
|
+
margin: "0",
|
|
4317
|
+
padding: "0",
|
|
4318
|
+
border: "0",
|
|
4319
|
+
background: "transparent",
|
|
4320
|
+
pointerEvents: "none",
|
|
4321
|
+
zIndex: "10001"
|
|
4322
|
+
});
|
|
4323
|
+
document.body.append(overlay);
|
|
4324
|
+
if (typeof overlay.showPopover === "function") {
|
|
4325
|
+
overlay.popover = "manual";
|
|
4326
|
+
overlay.showPopover();
|
|
4327
|
+
}
|
|
4328
|
+
return overlay;
|
|
4329
|
+
}
|
|
4330
|
+
function themeColors(themed) {
|
|
4331
|
+
const probe = document.createElement("span");
|
|
4332
|
+
probe.hidden = true;
|
|
4333
|
+
themed.append(probe);
|
|
4334
|
+
const style = getComputedStyle(probe);
|
|
4335
|
+
const colors = THEME_COLORS.filter((role) => style.getPropertyValue(`--md-sys-color-${role}`).trim()).map((role) => {
|
|
4336
|
+
probe.style.color = `var(--md-sys-color-${role})`;
|
|
4337
|
+
return getComputedStyle(probe).color;
|
|
4338
|
+
});
|
|
4339
|
+
probe.remove();
|
|
4340
|
+
return colors.length ? colors : FALLBACK_COLORS;
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
// src/ts/components/particles.ts
|
|
4344
|
+
var _defaults13 = {
|
|
4345
|
+
quantity: 100,
|
|
4346
|
+
staticity: 50,
|
|
4347
|
+
ease: 50,
|
|
4348
|
+
size: 0.4,
|
|
4349
|
+
color: "",
|
|
4350
|
+
vx: 0,
|
|
4351
|
+
vy: 0
|
|
4352
|
+
};
|
|
4353
|
+
var Particles = class _Particles extends Component {
|
|
4354
|
+
constructor(el, options) {
|
|
4355
|
+
super(el, options, _Particles);
|
|
4356
|
+
this._particles = [];
|
|
4357
|
+
this._width = 0;
|
|
4358
|
+
this._height = 0;
|
|
4359
|
+
this._frame = 0;
|
|
4360
|
+
this._last = 0;
|
|
4361
|
+
// Pointer offset from the canvas center, kept from its last move inside.
|
|
4362
|
+
this._mouse = { x: 0, y: 0 };
|
|
4363
|
+
this._still = false;
|
|
4364
|
+
this._resizeObserver = null;
|
|
4365
|
+
this._visibility = null;
|
|
4366
|
+
this._onPointerMove = (e) => {
|
|
4367
|
+
const r = this.el.getBoundingClientRect();
|
|
4368
|
+
const x = e.clientX - r.left - r.width / 2;
|
|
4369
|
+
const y = e.clientY - r.top - r.height / 2;
|
|
4370
|
+
if (Math.abs(x) < r.width / 2 && Math.abs(y) < r.height / 2) this._mouse = { x, y };
|
|
4371
|
+
};
|
|
4372
|
+
this.el["Expressive_Particles"] = this;
|
|
4373
|
+
this.options = { ..._Particles.defaults, ...options };
|
|
4374
|
+
this._ctx = this.el.getContext("2d");
|
|
4375
|
+
if (!this._ctx) return;
|
|
4376
|
+
this._style = getComputedStyle(this.el);
|
|
4377
|
+
this._still = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
4378
|
+
this._reset();
|
|
4379
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
4380
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
4381
|
+
if (this.el.clientWidth !== this._width || this.el.clientHeight !== this._height) this._reset();
|
|
4382
|
+
});
|
|
4383
|
+
this._resizeObserver.observe(this.el);
|
|
4384
|
+
}
|
|
4385
|
+
if (this._still) return;
|
|
4386
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
4387
|
+
this._start();
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
this._visibility = new IntersectionObserver(([entry]) => entry.isIntersecting ? this._start() : this._stop());
|
|
4391
|
+
this._visibility.observe(this.el);
|
|
4392
|
+
}
|
|
4393
|
+
static get defaults() {
|
|
4394
|
+
return _defaults13;
|
|
4395
|
+
}
|
|
4396
|
+
static init(els, options = {}) {
|
|
4397
|
+
return super.init(els, options, _Particles);
|
|
4398
|
+
}
|
|
4399
|
+
static getInstance(el) {
|
|
4400
|
+
return el["Expressive_Particles"];
|
|
4401
|
+
}
|
|
4402
|
+
destroy() {
|
|
4403
|
+
this._stop();
|
|
4404
|
+
this._resizeObserver?.disconnect();
|
|
4405
|
+
this._visibility?.disconnect();
|
|
4406
|
+
if (this._ctx) {
|
|
4407
|
+
this._ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4408
|
+
this._ctx.clearRect(0, 0, this.el.width, this.el.height);
|
|
4409
|
+
}
|
|
4410
|
+
this.el["Expressive_Particles"] = void 0;
|
|
4411
|
+
}
|
|
4412
|
+
_start() {
|
|
4413
|
+
if (this._frame) return;
|
|
4414
|
+
window.addEventListener("pointermove", this._onPointerMove, { passive: true });
|
|
4415
|
+
this._frame = requestAnimationFrame((now) => {
|
|
4416
|
+
this._last = now;
|
|
4417
|
+
this._draw(now);
|
|
4418
|
+
});
|
|
4419
|
+
}
|
|
4420
|
+
_stop() {
|
|
4421
|
+
cancelAnimationFrame(this._frame);
|
|
4422
|
+
this._frame = 0;
|
|
4423
|
+
window.removeEventListener("pointermove", this._onPointerMove);
|
|
4424
|
+
}
|
|
4425
|
+
// Matches the pixel buffer to the canvas box and scatters a new set.
|
|
4426
|
+
_reset() {
|
|
4427
|
+
this._width = this.el.clientWidth;
|
|
4428
|
+
this._height = this.el.clientHeight;
|
|
4429
|
+
this.el.width = Math.round(this._width * devicePixelRatio);
|
|
4430
|
+
this.el.height = Math.round(this._height * devicePixelRatio);
|
|
4431
|
+
this._particles = Array.from({ length: this.options.quantity }, () => this._particle());
|
|
4432
|
+
if (this._still) {
|
|
4433
|
+
for (const p of this._particles) p.alpha = p.targetAlpha;
|
|
4434
|
+
this._paint();
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
_particle() {
|
|
4438
|
+
return {
|
|
4439
|
+
x: Math.floor(Math.random() * this._width),
|
|
4440
|
+
y: Math.floor(Math.random() * this._height),
|
|
4441
|
+
translateX: 0,
|
|
4442
|
+
translateY: 0,
|
|
4443
|
+
size: Math.floor(Math.random() * 2) + this.options.size,
|
|
4444
|
+
alpha: 0,
|
|
4445
|
+
targetAlpha: Math.round((Math.random() * 0.6 + 0.1) * 10) / 10,
|
|
4446
|
+
dx: (Math.random() - 0.5) * 0.1,
|
|
4447
|
+
dy: (Math.random() - 0.5) * 0.1,
|
|
4448
|
+
magnetism: 0.1 + Math.random() * 4
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
_draw(now) {
|
|
4452
|
+
const step = Math.min((now - this._last) / (1e3 / 60), 4);
|
|
4453
|
+
this._last = now;
|
|
4454
|
+
const { staticity, ease, vx, vy } = this.options;
|
|
4455
|
+
const follow = Math.min(step / ease, 1);
|
|
4456
|
+
this._particles = this._particles.map((p) => {
|
|
4457
|
+
p.x += (p.dx + vx) * step;
|
|
4458
|
+
p.y += (p.dy + vy) * step;
|
|
4459
|
+
p.translateX += (this._mouse.x / (staticity / p.magnetism) - p.translateX) * follow;
|
|
4460
|
+
p.translateY += (this._mouse.y / (staticity / p.magnetism) - p.translateY) * follow;
|
|
4461
|
+
const x = p.x + p.translateX;
|
|
4462
|
+
const y = p.y + p.translateY;
|
|
4463
|
+
const edge = Math.min(x - p.size, this._width - x - p.size, y - p.size, this._height - y - p.size) / 20;
|
|
4464
|
+
p.alpha = edge > 1 ? Math.min(p.alpha + 0.02 * step, p.targetAlpha) : p.targetAlpha * edge;
|
|
4465
|
+
const gone = p.x < -p.size || p.x > this._width + p.size || p.y < -p.size || p.y > this._height + p.size;
|
|
4466
|
+
return gone ? this._particle() : p;
|
|
4467
|
+
});
|
|
4468
|
+
this._paint();
|
|
4469
|
+
this._frame = requestAnimationFrame((t) => this._draw(t));
|
|
4470
|
+
}
|
|
4471
|
+
_paint() {
|
|
4472
|
+
const ctx = this._ctx;
|
|
4473
|
+
const dpr = devicePixelRatio;
|
|
4474
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4475
|
+
ctx.clearRect(0, 0, this.el.width, this.el.height);
|
|
4476
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
4477
|
+
ctx.fillStyle = this.options.color || this._style.color;
|
|
4478
|
+
for (const p of this._particles) {
|
|
4479
|
+
if (p.alpha <= 0) continue;
|
|
4480
|
+
ctx.globalAlpha = p.alpha;
|
|
4481
|
+
ctx.beginPath();
|
|
4482
|
+
ctx.arc(p.x + p.translateX, p.y + p.translateY, p.size, 0, 2 * Math.PI);
|
|
4483
|
+
ctx.fill();
|
|
4484
|
+
}
|
|
4485
|
+
ctx.globalAlpha = 1;
|
|
4486
|
+
}
|
|
4487
|
+
};
|
|
4488
|
+
|
|
4489
|
+
// src/ts/components/commandPalette.ts
|
|
4490
|
+
var _defaults14 = {
|
|
4491
|
+
shortcut: "k",
|
|
4492
|
+
closeOnRun: true
|
|
4493
|
+
};
|
|
4494
|
+
var APPLE = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
4495
|
+
var fold = (text) => text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
|
|
4496
|
+
var CommandPalette = class _CommandPalette extends Component {
|
|
4497
|
+
constructor(el, options) {
|
|
4498
|
+
super(el, options, _CommandPalette);
|
|
4499
|
+
this._active = null;
|
|
4500
|
+
this._generatedIds = [];
|
|
4501
|
+
/** Wires and refilters commands the page adds or removes later. */
|
|
4502
|
+
this._observer = null;
|
|
4503
|
+
/** Opens the palette as a modal dialog with the search input focused. */
|
|
4504
|
+
this.open = () => {
|
|
4505
|
+
if (!this.el.open) this.el.showModal();
|
|
4506
|
+
this.input?.focus();
|
|
4507
|
+
};
|
|
4508
|
+
/** Closes the palette. The dialog returns focus to where it was. */
|
|
4509
|
+
this.close = () => {
|
|
4510
|
+
if (this.el.open) this.el.close();
|
|
4511
|
+
};
|
|
4512
|
+
this._handleInput = () => this._filter();
|
|
4513
|
+
this._handleKeydown = (e) => {
|
|
4514
|
+
if (e.key === Utils.keys.ARROW_DOWN || e.key === Utils.keys.ARROW_UP) {
|
|
4515
|
+
e.preventDefault();
|
|
4516
|
+
this._move(e.key === Utils.keys.ARROW_DOWN ? 1 : -1);
|
|
4517
|
+
} else if (e.key === Utils.keys.ENTER && !e.isComposing && e.keyCode !== 229 && this._active) {
|
|
4518
|
+
e.preventDefault();
|
|
4519
|
+
this._run(this._active);
|
|
4520
|
+
}
|
|
4521
|
+
};
|
|
4522
|
+
this._handleListClick = (e) => {
|
|
4523
|
+
const item = e.target.closest('[role="option"]');
|
|
4524
|
+
if (!item || !this.list.contains(item) || item.getAttribute("aria-disabled") === "true") return;
|
|
4525
|
+
const action = this._action(item);
|
|
4526
|
+
if (action && !action.contains(e.target)) {
|
|
4527
|
+
action.click();
|
|
4528
|
+
return;
|
|
4529
|
+
}
|
|
4530
|
+
if (this.options.closeOnRun) this.close();
|
|
4531
|
+
};
|
|
4532
|
+
this._handlePointerMove = (e) => {
|
|
4533
|
+
const item = e.target.closest('[role="option"]');
|
|
4534
|
+
if (item && this.list.contains(item) && item.getAttribute("aria-disabled") !== "true") this._setActive(item);
|
|
4535
|
+
};
|
|
4536
|
+
/**
|
|
4537
|
+
* Each opening starts from the full list. The close event is queued, so a
|
|
4538
|
+
* palette reopened straight away may already be open again; its new search
|
|
4539
|
+
* is left alone.
|
|
4540
|
+
*/
|
|
4541
|
+
this._handleClose = () => {
|
|
4542
|
+
if (this.el.open) return;
|
|
4543
|
+
this.input.value = "";
|
|
4544
|
+
this._filter();
|
|
4545
|
+
};
|
|
4546
|
+
this._handleShortcut = (e) => {
|
|
4547
|
+
const { shortcut } = this.options;
|
|
4548
|
+
if (!shortcut || e.defaultPrevented || e.altKey || e.shiftKey) return;
|
|
4549
|
+
if (!(APPLE ? e.metaKey && !e.ctrlKey : e.ctrlKey && !e.metaKey)) return;
|
|
4550
|
+
const letter = shortcut.toLowerCase();
|
|
4551
|
+
const latin = /^[a-z]$/i.test(e.key);
|
|
4552
|
+
const hit = e.key.toLowerCase() === letter || !latin && /^[a-z]$/.test(letter) && e.code === `Key${letter.toUpperCase()}`;
|
|
4553
|
+
if (!hit) return;
|
|
4554
|
+
e.preventDefault();
|
|
4555
|
+
if (this.el.open) this.close();
|
|
4556
|
+
else this.open();
|
|
4557
|
+
};
|
|
4558
|
+
this.el["Expressive_CommandPalette"] = this;
|
|
4559
|
+
const shortcut = el.dataset.shortcut;
|
|
4560
|
+
this.options = {
|
|
4561
|
+
..._CommandPalette.defaults,
|
|
4562
|
+
...shortcut !== void 0 ? { shortcut: shortcut || null } : {},
|
|
4563
|
+
...options
|
|
4564
|
+
};
|
|
4565
|
+
this.input = el.querySelector(":scope > input");
|
|
4566
|
+
this.list = el.querySelector(":scope > :is(ul, ol)");
|
|
4567
|
+
if (!this.input || !this.list) {
|
|
4568
|
+
console.error("CommandPalette needs an <input> and a <ul> as children of the dialog.");
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4571
|
+
this._setupAccessibility();
|
|
4572
|
+
this._filter();
|
|
4573
|
+
this._observer = new MutationObserver(() => this._filter(true));
|
|
4574
|
+
this._observer.observe(this.list, {
|
|
4575
|
+
childList: true,
|
|
4576
|
+
subtree: true,
|
|
4577
|
+
attributes: true,
|
|
4578
|
+
attributeFilter: ["hidden", "disabled"]
|
|
4579
|
+
});
|
|
4580
|
+
this.input.addEventListener("input", this._handleInput);
|
|
4581
|
+
this.input.addEventListener("keydown", this._handleKeydown);
|
|
4582
|
+
this.list.addEventListener("click", this._handleListClick);
|
|
4583
|
+
this.list.addEventListener("pointermove", this._handlePointerMove);
|
|
4584
|
+
this.el.addEventListener("close", this._handleClose);
|
|
4585
|
+
document.addEventListener("keydown", this._handleShortcut);
|
|
4586
|
+
}
|
|
4587
|
+
static get defaults() {
|
|
4588
|
+
return _defaults14;
|
|
4589
|
+
}
|
|
4590
|
+
/**
|
|
4591
|
+
* Initializes instances of CommandPalette.
|
|
4592
|
+
* @param els HTML elements.
|
|
4593
|
+
* @param options Component options.
|
|
4594
|
+
*/
|
|
4595
|
+
static init(els, options = {}) {
|
|
4596
|
+
return super.init(els, options, _CommandPalette);
|
|
4597
|
+
}
|
|
4598
|
+
static getInstance(el) {
|
|
4599
|
+
return el["Expressive_CommandPalette"];
|
|
4600
|
+
}
|
|
4601
|
+
/** If the palette is open. */
|
|
4602
|
+
get isOpen() {
|
|
4603
|
+
return this.el.open;
|
|
4604
|
+
}
|
|
4605
|
+
destroy() {
|
|
4606
|
+
this._observer?.disconnect();
|
|
4607
|
+
document.removeEventListener("keydown", this._handleShortcut);
|
|
4608
|
+
this.el.removeEventListener("close", this._handleClose);
|
|
4609
|
+
if (this.input && this.list) {
|
|
4610
|
+
this.input.removeEventListener("input", this._handleInput);
|
|
4611
|
+
this.input.removeEventListener("keydown", this._handleKeydown);
|
|
4612
|
+
this.list.removeEventListener("click", this._handleListClick);
|
|
4613
|
+
this.list.removeEventListener("pointermove", this._handlePointerMove);
|
|
4614
|
+
for (const name of ["role", "aria-controls", "aria-expanded", "aria-autocomplete", "aria-activedescendant"]) {
|
|
4615
|
+
this.input.removeAttribute(name);
|
|
4616
|
+
}
|
|
4617
|
+
this.list.removeAttribute("role");
|
|
4618
|
+
for (const item of this._items()) {
|
|
4619
|
+
for (const name of ["role", "aria-selected", "aria-disabled", "data-filtered"]) item.removeAttribute(name);
|
|
4620
|
+
item.classList.remove("active");
|
|
4621
|
+
this._action(item)?.removeAttribute("tabindex");
|
|
4622
|
+
}
|
|
4623
|
+
for (const label of this._labels()) {
|
|
4624
|
+
label.removeAttribute("role");
|
|
4625
|
+
label.removeAttribute("data-filtered");
|
|
4626
|
+
}
|
|
4627
|
+
for (const el of this._generatedIds) el.removeAttribute("id");
|
|
4628
|
+
const empty = this._empty();
|
|
4629
|
+
if (empty) empty.hidden = false;
|
|
4630
|
+
}
|
|
4631
|
+
this.el["Expressive_CommandPalette"] = void 0;
|
|
4632
|
+
}
|
|
4633
|
+
_items() {
|
|
4634
|
+
return Array.from(this.list.children).filter(
|
|
4635
|
+
(child) => child instanceof HTMLElement && !child.classList.contains("label")
|
|
4636
|
+
);
|
|
4637
|
+
}
|
|
4638
|
+
_labels() {
|
|
4639
|
+
return Array.from(this.list.querySelectorAll(":scope > .label"));
|
|
4640
|
+
}
|
|
4641
|
+
_empty() {
|
|
4642
|
+
return this.el.querySelector(":scope > .command-palette-empty");
|
|
4643
|
+
}
|
|
4644
|
+
_action(item) {
|
|
4645
|
+
return item.querySelector(":scope > :is(a[href], button)");
|
|
4646
|
+
}
|
|
4647
|
+
_ensureId(el, prefix) {
|
|
4648
|
+
if (!el.id) {
|
|
4649
|
+
el.id = `${prefix}-${Utils.guid()}`;
|
|
4650
|
+
this._generatedIds.push(el);
|
|
4651
|
+
}
|
|
4652
|
+
return el.id;
|
|
4653
|
+
}
|
|
4654
|
+
_setupAccessibility() {
|
|
4655
|
+
this.list.setAttribute("role", "listbox");
|
|
4656
|
+
this.input.setAttribute("role", "combobox");
|
|
4657
|
+
this.input.setAttribute("aria-controls", this._ensureId(this.list, "command-palette-list"));
|
|
4658
|
+
this.input.setAttribute("aria-expanded", "true");
|
|
4659
|
+
this.input.setAttribute("aria-autocomplete", "list");
|
|
4660
|
+
}
|
|
4661
|
+
/** Every command, including ones added since the last pass, is an option. */
|
|
4662
|
+
_wireItems() {
|
|
4663
|
+
for (const item of this._items()) {
|
|
4664
|
+
if (item.getAttribute("role") === "option") continue;
|
|
4665
|
+
item.setAttribute("role", "option");
|
|
4666
|
+
this._ensureId(item, "command-palette-option");
|
|
4667
|
+
this._action(item)?.setAttribute("tabindex", "-1");
|
|
4668
|
+
}
|
|
4669
|
+
for (const label of this._labels()) label.setAttribute("role", "none");
|
|
4670
|
+
}
|
|
4671
|
+
/** Commands on screen: neither hidden by the page nor filtered out. */
|
|
4672
|
+
_shown() {
|
|
4673
|
+
return this._items().filter((item) => !item.hidden && !item.hasAttribute("data-filtered"));
|
|
4674
|
+
}
|
|
4675
|
+
/** Shown commands the arrows and Enter can reach: disabled ones are skipped. */
|
|
4676
|
+
_visible() {
|
|
4677
|
+
return this._shown().filter((item) => item.getAttribute("aria-disabled") !== "true");
|
|
4678
|
+
}
|
|
4679
|
+
/** A disabled link or button cannot run, so its option says so. */
|
|
4680
|
+
_isDisabled(item) {
|
|
4681
|
+
const action = this._action(item);
|
|
4682
|
+
return !!action && (action.matches(":disabled") || action.getAttribute("aria-disabled") === "true");
|
|
4683
|
+
}
|
|
4684
|
+
_setActive(item) {
|
|
4685
|
+
if (this._active === item) return;
|
|
4686
|
+
this._active?.classList.remove("active");
|
|
4687
|
+
this._active?.removeAttribute("aria-selected");
|
|
4688
|
+
this._active = item;
|
|
4689
|
+
if (item) {
|
|
4690
|
+
item.classList.add("active");
|
|
4691
|
+
item.setAttribute("aria-selected", "true");
|
|
4692
|
+
this.input.setAttribute("aria-activedescendant", item.id);
|
|
4693
|
+
item.scrollIntoView({ block: "nearest" });
|
|
4694
|
+
} else {
|
|
4695
|
+
this.input.removeAttribute("aria-activedescendant");
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
/**
|
|
4699
|
+
* Shows the commands matching the query. Filtering marks rows with
|
|
4700
|
+
* data-filtered rather than hidden, so a command the page hides stays
|
|
4701
|
+
* hidden. `keepActive` holds the active command while it is still
|
|
4702
|
+
* reachable, for list changes the user did not make.
|
|
4703
|
+
*/
|
|
4704
|
+
_filter(keepActive = false) {
|
|
4705
|
+
this._wireItems();
|
|
4706
|
+
const terms = fold(this.input.value).split(/\s+/).filter(Boolean);
|
|
4707
|
+
for (const item of this._items()) {
|
|
4708
|
+
const keywords = item.dataset.keywords ?? this._action(item)?.dataset.keywords ?? "";
|
|
4709
|
+
const text = fold(`${item.textContent} ${keywords}`);
|
|
4710
|
+
item.toggleAttribute("data-filtered", !terms.every((term) => text.includes(term)));
|
|
4711
|
+
if (this._isDisabled(item)) item.setAttribute("aria-disabled", "true");
|
|
4712
|
+
else item.removeAttribute("aria-disabled");
|
|
4713
|
+
}
|
|
4714
|
+
for (const label of this._labels()) {
|
|
4715
|
+
let next = label.nextElementSibling;
|
|
4716
|
+
let any = false;
|
|
4717
|
+
while (next && !next.classList.contains("label")) {
|
|
4718
|
+
if (!next.hidden && !next.hasAttribute("data-filtered")) any = true;
|
|
4719
|
+
next = next.nextElementSibling;
|
|
4720
|
+
}
|
|
4721
|
+
label.toggleAttribute("data-filtered", !any);
|
|
4722
|
+
}
|
|
4723
|
+
const visible = this._visible();
|
|
4724
|
+
const empty = this._empty();
|
|
4725
|
+
if (empty) empty.hidden = this._shown().length > 0;
|
|
4726
|
+
this._setActive(keepActive && visible.includes(this._active) ? this._active : visible[0] ?? null);
|
|
4727
|
+
}
|
|
4728
|
+
_move(step) {
|
|
4729
|
+
const visible = this._visible();
|
|
4730
|
+
if (!visible.length) return;
|
|
4731
|
+
const index = visible.indexOf(this._active);
|
|
4732
|
+
this._setActive(visible[(index + step + visible.length) % visible.length]);
|
|
4733
|
+
}
|
|
4734
|
+
/** Runs a command by clicking its link or button; the click handler closes. */
|
|
4735
|
+
_run(item) {
|
|
4736
|
+
(this._action(item) ?? item).click();
|
|
4737
|
+
}
|
|
4738
|
+
};
|
|
4739
|
+
|
|
4740
|
+
// src/ts/components/questionnaire.ts
|
|
4741
|
+
var _defaults15 = {
|
|
4742
|
+
progressLabel: "Question {current} of {total}",
|
|
4743
|
+
requiredMessage: "Choose an answer to continue."
|
|
4744
|
+
};
|
|
4745
|
+
var NAVIGATION = ["previous", "skip", "next"];
|
|
4746
|
+
var Questionnaire = class _Questionnaire extends Component {
|
|
4747
|
+
constructor(el, options) {
|
|
4748
|
+
super(el, options, _Questionnaire);
|
|
4749
|
+
this._active = null;
|
|
4750
|
+
this._generated = [];
|
|
4751
|
+
this._generatedIds = [];
|
|
4752
|
+
/** Set while one submission's invalid events arrive, so only the first opens. */
|
|
4753
|
+
this._reporting = false;
|
|
4754
|
+
/** Shows the question at `index` without checking the current one. */
|
|
4755
|
+
this.goTo = (index) => {
|
|
4756
|
+
const item = this.items[index];
|
|
4757
|
+
if (!item) return;
|
|
4758
|
+
this._active = item;
|
|
4759
|
+
this._render();
|
|
4760
|
+
const controls = this._controls(item).filter((control) => !control.disabled);
|
|
4761
|
+
(controls.find((control) => control.checked) ?? controls[0])?.focus();
|
|
4762
|
+
};
|
|
4763
|
+
/** Checks the shown question and moves to the next one if it is answered. */
|
|
4764
|
+
this.next = () => {
|
|
4765
|
+
if (this._validate(this._active)) this.goTo(this.index + 1);
|
|
4766
|
+
};
|
|
4767
|
+
/** Moves to the previous question. */
|
|
4768
|
+
this.previous = () => this.goTo(this.index - 1);
|
|
4769
|
+
/** Clears the answer to the shown question and moves to the next one. */
|
|
4770
|
+
this.skip = () => {
|
|
4771
|
+
if (!this._active) return;
|
|
4772
|
+
for (const control of this._controls(this._active)) {
|
|
4773
|
+
const isChoice = control instanceof HTMLInputElement && (control.type === "radio" || control.type === "checkbox");
|
|
4774
|
+
if (isChoice ? !control.checked : !control.value) continue;
|
|
4775
|
+
if (isChoice) control.checked = false;
|
|
4776
|
+
else control.value = "";
|
|
4777
|
+
control.dispatchEvent(new Event("change", { bubbles: true }));
|
|
4778
|
+
}
|
|
4779
|
+
this.goTo(this.index + 1);
|
|
4780
|
+
};
|
|
4781
|
+
this._handleClick = (e) => {
|
|
4782
|
+
const button = e.target.closest?.("button");
|
|
4783
|
+
if (!button || !this._buttons().includes(button)) return;
|
|
4784
|
+
if (NAVIGATION.includes(button.value)) {
|
|
4785
|
+
e.preventDefault();
|
|
4786
|
+
this[button.value]();
|
|
4787
|
+
return;
|
|
4788
|
+
}
|
|
4789
|
+
this._syncRequired();
|
|
4790
|
+
};
|
|
4791
|
+
// Enter in a field moves on instead of submitting the form early.
|
|
4792
|
+
this._handleKeydown = (e) => {
|
|
4793
|
+
if (e.key !== "Enter" || e.defaultPrevented || e.isComposing) return;
|
|
4794
|
+
if (!(e.target instanceof HTMLInputElement) || this.index >= this.items.length - 1) return;
|
|
4795
|
+
e.preventDefault();
|
|
4796
|
+
this.next();
|
|
4797
|
+
};
|
|
4798
|
+
// A submission found an unanswered question, possibly a hidden one. Open the
|
|
4799
|
+
// first instead of letting the browser report a control it cannot show.
|
|
4800
|
+
// One submission fires its invalid events in a single task.
|
|
4801
|
+
this._handleInvalid = (e) => {
|
|
4802
|
+
const item = this.items.find((item2) => item2.contains(e.target));
|
|
4803
|
+
if (!item) return;
|
|
4804
|
+
e.preventDefault();
|
|
4805
|
+
if (this._reporting) return;
|
|
4806
|
+
this._reporting = true;
|
|
4807
|
+
setTimeout(() => this._reporting = false);
|
|
4808
|
+
this._active = item;
|
|
4809
|
+
this._render();
|
|
4810
|
+
this._validate(item);
|
|
4811
|
+
};
|
|
4812
|
+
// An answer clears the question's error.
|
|
4813
|
+
this._handleInput = (e) => {
|
|
4814
|
+
this._syncRequired();
|
|
4815
|
+
const item = e.target.closest?.("fieldset");
|
|
4816
|
+
if (item instanceof HTMLFieldSetElement && item.classList.contains("invalid")) this._setInvalid(item, false);
|
|
4817
|
+
};
|
|
4818
|
+
this.el["Expressive_Questionnaire"] = this;
|
|
4819
|
+
const progressLabel = el.dataset.progressLabel;
|
|
4820
|
+
this.options = {
|
|
4821
|
+
..._Questionnaire.defaults,
|
|
4822
|
+
...progressLabel ? { progressLabel } : {},
|
|
4823
|
+
...options
|
|
4824
|
+
};
|
|
4825
|
+
this._progress = document.createElement("div");
|
|
4826
|
+
this._progress.className = "questionnaire-progress";
|
|
4827
|
+
this._progressText = document.createElement("span");
|
|
4828
|
+
this._progressText.id = `questionnaire-progress-${Utils.guid()}`;
|
|
4829
|
+
this._progressBar = document.createElement("progress");
|
|
4830
|
+
this._progressBar.className = "progress";
|
|
4831
|
+
this._progressBar.setAttribute("aria-labelledby", this._progressText.id);
|
|
4832
|
+
this._progress.append(this._progressText, this._progressBar);
|
|
4833
|
+
this.el.prepend(this._progress);
|
|
4834
|
+
for (const item of this._fieldsets()) {
|
|
4835
|
+
const ids = Array.from(item.querySelectorAll(":scope > p:not(.questionnaire-error)")).map((p) => this._ensureId(p, "questionnaire-description"));
|
|
4836
|
+
if (ids.length && !item.hasAttribute("aria-describedby")) {
|
|
4837
|
+
item.setAttribute("aria-describedby", ids.join(" "));
|
|
4838
|
+
this._generatedIds.push(item);
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
this._active = this.items[0] ?? null;
|
|
4842
|
+
this._syncRequired();
|
|
4843
|
+
this._render();
|
|
4844
|
+
this._observer = new MutationObserver(() => {
|
|
4845
|
+
this._syncRequired();
|
|
4846
|
+
this._render();
|
|
4847
|
+
});
|
|
4848
|
+
this._observer.observe(this.el, { attributes: true, subtree: true, attributeFilter: ["disabled"] });
|
|
4849
|
+
this.el.addEventListener("click", this._handleClick);
|
|
4850
|
+
this.el.addEventListener("keydown", this._handleKeydown);
|
|
4851
|
+
this.el.addEventListener("input", this._handleInput);
|
|
4852
|
+
this.el.addEventListener("invalid", this._handleInvalid, true);
|
|
4853
|
+
}
|
|
4854
|
+
static get defaults() {
|
|
4855
|
+
return _defaults15;
|
|
4856
|
+
}
|
|
4857
|
+
/**
|
|
4858
|
+
* Initializes instances of Questionnaire.
|
|
4859
|
+
* @param els HTML elements.
|
|
4860
|
+
* @param options Component options.
|
|
4861
|
+
*/
|
|
4862
|
+
static init(els, options = {}) {
|
|
4863
|
+
return super.init(els, options, _Questionnaire);
|
|
4864
|
+
}
|
|
4865
|
+
static getInstance(el) {
|
|
4866
|
+
return el["Expressive_Questionnaire"];
|
|
4867
|
+
}
|
|
4868
|
+
/** The questions that apply: every fieldset child that is not disabled. */
|
|
4869
|
+
get items() {
|
|
4870
|
+
return this._fieldsets().filter((item) => !item.disabled);
|
|
4871
|
+
}
|
|
4872
|
+
/** Position of the shown question in `items`. */
|
|
4873
|
+
get index() {
|
|
4874
|
+
return this.items.indexOf(this._active);
|
|
4875
|
+
}
|
|
4876
|
+
destroy() {
|
|
4877
|
+
this._observer.disconnect();
|
|
4878
|
+
this.el.removeEventListener("click", this._handleClick);
|
|
4879
|
+
this.el.removeEventListener("keydown", this._handleKeydown);
|
|
4880
|
+
this.el.removeEventListener("input", this._handleInput);
|
|
4881
|
+
this.el.removeEventListener("invalid", this._handleInvalid, true);
|
|
4882
|
+
for (const item of this._fieldsets()) {
|
|
4883
|
+
if (item.hasAttribute("data-required")) for (const control of this._controls(item)) control.setCustomValidity("");
|
|
4884
|
+
item.hidden = false;
|
|
4885
|
+
this._setInvalid(item, false);
|
|
4886
|
+
}
|
|
4887
|
+
for (const button of this._buttons()) button.hidden = false;
|
|
4888
|
+
this._progress.remove();
|
|
4889
|
+
for (const el of this._generated) el.remove();
|
|
4890
|
+
for (const el of this._generatedIds) {
|
|
4891
|
+
el.removeAttribute(el instanceof HTMLFieldSetElement ? "aria-describedby" : "id");
|
|
4892
|
+
}
|
|
4893
|
+
this.el["Expressive_Questionnaire"] = void 0;
|
|
4894
|
+
}
|
|
4895
|
+
_fieldsets() {
|
|
4896
|
+
return Array.from(this.el.querySelectorAll(":scope > fieldset"));
|
|
4897
|
+
}
|
|
4898
|
+
/** The footer's navigation and submit buttons. */
|
|
4899
|
+
_buttons() {
|
|
4900
|
+
return Array.from(this.el.querySelectorAll(":scope > footer > button")).filter(
|
|
4901
|
+
(button) => NAVIGATION.includes(button.value) || button.type === "submit"
|
|
4902
|
+
);
|
|
4903
|
+
}
|
|
4904
|
+
_controls(item) {
|
|
4905
|
+
return Array.from(item.elements).filter(
|
|
4906
|
+
(el) => el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement || el instanceof HTMLInputElement && el.type !== "button" && el.type !== "submit"
|
|
4907
|
+
);
|
|
4908
|
+
}
|
|
4909
|
+
_required(item) {
|
|
4910
|
+
return item.hasAttribute("data-required") || this._controls(item).some((control) => control.required);
|
|
4911
|
+
}
|
|
4912
|
+
_ensureId(el, prefix) {
|
|
4913
|
+
if (!el.id) {
|
|
4914
|
+
el.id = `${prefix}-${Utils.guid()}`;
|
|
4915
|
+
this._generatedIds.push(el);
|
|
4916
|
+
}
|
|
4917
|
+
return el.id;
|
|
4918
|
+
}
|
|
4919
|
+
_render() {
|
|
4920
|
+
const items = this.items;
|
|
4921
|
+
if (!items.includes(this._active)) {
|
|
4922
|
+
const all = this._fieldsets();
|
|
4923
|
+
const after = all.slice(all.indexOf(this._active) + 1).find((item) => !item.disabled);
|
|
4924
|
+
this._active = after ?? items[items.length - 1] ?? null;
|
|
4925
|
+
}
|
|
4926
|
+
const index = items.indexOf(this._active);
|
|
4927
|
+
const last = index === items.length - 1;
|
|
4928
|
+
for (const item of this._fieldsets()) item.hidden = item !== this._active;
|
|
4929
|
+
for (const button of this._buttons()) {
|
|
4930
|
+
if (button.value === "previous") button.hidden = index <= 0;
|
|
4931
|
+
else if (button.value === "skip") button.hidden = last || !this._active || this._required(this._active);
|
|
4932
|
+
else if (button.value === "next") button.hidden = last;
|
|
4933
|
+
else button.hidden = !last;
|
|
4934
|
+
}
|
|
4935
|
+
this._progressText.textContent = this.options.progressLabel.replace("{current}", String(index + 1)).replace("{total}", String(items.length));
|
|
4936
|
+
this._progressBar.max = Math.max(items.length, 1);
|
|
4937
|
+
this._progressBar.value = index + 1;
|
|
4938
|
+
}
|
|
4939
|
+
/**
|
|
4940
|
+
* Puts "choose at least one" on the first enabled checkbox of each
|
|
4941
|
+
* data-required group with none checked, so the browser's validation, and
|
|
4942
|
+
* every way of submitting, sees it. A disabled checkbox is not sent, so it
|
|
4943
|
+
* does not count.
|
|
4944
|
+
*/
|
|
4945
|
+
_syncRequired() {
|
|
4946
|
+
for (const item of this._fieldsets()) {
|
|
4947
|
+
if (!item.hasAttribute("data-required")) continue;
|
|
4948
|
+
const boxes = this._controls(item).filter(
|
|
4949
|
+
(control) => control instanceof HTMLInputElement && control.type === "checkbox"
|
|
4950
|
+
);
|
|
4951
|
+
const enabled = boxes.filter((box) => !box.disabled);
|
|
4952
|
+
for (const box of boxes) box.setCustomValidity("");
|
|
4953
|
+
if (enabled.length && !enabled.some((box) => box.checked)) {
|
|
4954
|
+
const error = item.querySelector(":scope > .questionnaire-error:not([data-generated])");
|
|
4955
|
+
enabled[0].setCustomValidity(error?.textContent.trim() || this.options.requiredMessage);
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
/** The first control in the question that still needs an answer. */
|
|
4960
|
+
_invalidControl(item) {
|
|
4961
|
+
return this._controls(item).find((control) => control.willValidate && !control.validity.valid);
|
|
4962
|
+
}
|
|
4963
|
+
/** Shows the question's error and returns false when it needs an answer. */
|
|
4964
|
+
_validate(item) {
|
|
4965
|
+
this._syncRequired();
|
|
4966
|
+
const invalid = item && this._invalidControl(item);
|
|
4967
|
+
if (!invalid) return true;
|
|
4968
|
+
let error = item.querySelector(":scope > .questionnaire-error");
|
|
4969
|
+
if (!error) {
|
|
4970
|
+
error = document.createElement("p");
|
|
4971
|
+
error.className = "questionnaire-error";
|
|
4972
|
+
error.dataset.generated = "";
|
|
4973
|
+
error.textContent = invalid.validationMessage || this.options.requiredMessage;
|
|
4974
|
+
item.append(error);
|
|
4975
|
+
this._generated.push(error);
|
|
4976
|
+
}
|
|
4977
|
+
this._setInvalid(item, true);
|
|
4978
|
+
invalid.focus();
|
|
4979
|
+
return false;
|
|
4980
|
+
}
|
|
4981
|
+
_setInvalid(item, invalid) {
|
|
4982
|
+
item.classList.toggle("invalid", invalid);
|
|
4983
|
+
const error = item.querySelector(":scope > .questionnaire-error");
|
|
4984
|
+
const described = (item.getAttribute("aria-describedby") ?? "").split(" ").filter((id) => id && id !== error?.id);
|
|
4985
|
+
if (invalid && error) described.push(this._ensureId(error, "questionnaire-error"));
|
|
4986
|
+
if (described.length) item.setAttribute("aria-describedby", described.join(" "));
|
|
4987
|
+
else item.removeAttribute("aria-describedby");
|
|
4988
|
+
for (const control of this._controls(item)) {
|
|
4989
|
+
if (invalid) control.setAttribute("aria-invalid", "true");
|
|
4990
|
+
else control.removeAttribute("aria-invalid");
|
|
4991
|
+
}
|
|
4992
|
+
if (!invalid && this._generated.includes(error)) {
|
|
4993
|
+
error.remove();
|
|
4994
|
+
this._generated.splice(this._generated.indexOf(error), 1);
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4997
|
+
};
|
|
4998
|
+
|
|
4999
|
+
// src/ts/components/lineChart.ts
|
|
5000
|
+
var _defaults16 = {
|
|
5001
|
+
min: null,
|
|
5002
|
+
max: null
|
|
5003
|
+
};
|
|
5004
|
+
var SVG = "http://www.w3.org/2000/svg";
|
|
5005
|
+
var COLORS = 4;
|
|
5006
|
+
function svgNode(name, attributes = {}) {
|
|
5007
|
+
const element = document.createElementNS(SVG, name);
|
|
5008
|
+
for (const key in attributes) element.setAttribute(key, attributes[key]);
|
|
5009
|
+
return element;
|
|
5010
|
+
}
|
|
5011
|
+
var _gradientId = 0;
|
|
5012
|
+
function cellValue(cell) {
|
|
5013
|
+
if (!cell) return null;
|
|
5014
|
+
const raw = cell.dataset.value ?? cell.textContent;
|
|
5015
|
+
const text = raw.replace(/\u2212/g, "-").replace(/[\s%\p{Sc}]/gu, "");
|
|
5016
|
+
if (!/^[+-]?(\d{1,3}(,\d{3})+|\d+)?(\.\d+)?(e[+-]?\d+)?$/i.test(text) || !/\d/.test(text)) return null;
|
|
5017
|
+
return parseFloat(text.replace(/,/g, ""));
|
|
5018
|
+
}
|
|
5019
|
+
function monotonePath(points) {
|
|
5020
|
+
const n = points.length;
|
|
5021
|
+
if (n === 0) return "";
|
|
5022
|
+
if (n === 1) return `M${points[0][0]},${points[0][1]}h0`;
|
|
5023
|
+
const slopes = [];
|
|
5024
|
+
for (let i = 0; i < n - 1; i++) {
|
|
5025
|
+
slopes.push((points[i + 1][1] - points[i][1]) / (points[i + 1][0] - points[i][0]));
|
|
5026
|
+
}
|
|
5027
|
+
const tangents = points.map((_, i) => {
|
|
5028
|
+
if (i === 0) return slopes[0];
|
|
5029
|
+
if (i === n - 1) return slopes[n - 2];
|
|
5030
|
+
const [a, b] = [slopes[i - 1], slopes[i]];
|
|
5031
|
+
return a * b <= 0 ? 0 : 2 / (1 / a + 1 / b);
|
|
5032
|
+
});
|
|
5033
|
+
let d = `M${points[0][0]},${points[0][1]}`;
|
|
5034
|
+
for (let i = 0; i < n - 1; i++) {
|
|
5035
|
+
const [[x0, y0], [x1, y1]] = [points[i], points[i + 1]];
|
|
5036
|
+
const h = (x1 - x0) / 3;
|
|
5037
|
+
d += `C${x0 + h},${y0 + tangents[i] * h} ${x1 - h},${y1 - tangents[i + 1] * h} ${x1},${y1}`;
|
|
5038
|
+
}
|
|
5039
|
+
return d;
|
|
5040
|
+
}
|
|
5041
|
+
var LineChart = class extends Component {
|
|
5042
|
+
constructor(el, options) {
|
|
5043
|
+
super(el, options, new.target);
|
|
5044
|
+
/** The values' x positions and series, read from the table. */
|
|
5045
|
+
this.labels = [];
|
|
5046
|
+
this.series = [];
|
|
5047
|
+
/** The highlighted row, or -1. */
|
|
5048
|
+
this.activeIndex = -1;
|
|
5049
|
+
this._generated = [];
|
|
5050
|
+
/** The table's header cells and body rows, as read. */
|
|
5051
|
+
this._header = [];
|
|
5052
|
+
this._rows = [];
|
|
5053
|
+
/**
|
|
5054
|
+
* Whether the rows run right to left: a horizontal row axis in a
|
|
5055
|
+
* right-to-left context, like the labels. Bar chart rows keep their order.
|
|
5056
|
+
*/
|
|
5057
|
+
this._reversed = false;
|
|
5058
|
+
this._onPointerMove = (e) => {
|
|
5059
|
+
this.show(this._indexAt(e));
|
|
5060
|
+
};
|
|
5061
|
+
this._onFocus = () => {
|
|
5062
|
+
if (this.activeIndex < 0 && this._plot.matches(":focus-visible")) this.show(0);
|
|
5063
|
+
};
|
|
5064
|
+
this._onLeave = (e) => {
|
|
5065
|
+
if (e.pointerType === "touch" || this._plot.matches(":focus-visible")) return;
|
|
5066
|
+
this.show(-1);
|
|
5067
|
+
};
|
|
5068
|
+
/** The browser took the touch to scroll the page. */
|
|
5069
|
+
this._onCancel = () => {
|
|
5070
|
+
this.show(-1);
|
|
5071
|
+
};
|
|
5072
|
+
this._onKeyDown = (e) => {
|
|
5073
|
+
const next = this._keyTarget(e.key);
|
|
5074
|
+
if (next === void 0) return;
|
|
5075
|
+
e.preventDefault();
|
|
5076
|
+
this.show(next);
|
|
5077
|
+
};
|
|
5078
|
+
this.el[this._key] = this;
|
|
5079
|
+
this.options = {
|
|
5080
|
+
...new.target.defaults,
|
|
5081
|
+
...options
|
|
5082
|
+
};
|
|
5083
|
+
this._read();
|
|
5084
|
+
if (!this.labels.length || !this.series.length) return;
|
|
5085
|
+
this._reversed = !this._horizontal && getComputedStyle(this.el).direction === "rtl";
|
|
5086
|
+
this._render();
|
|
5087
|
+
if (!this.el.classList.contains("sparkline")) {
|
|
5088
|
+
this._plot.tabIndex = 0;
|
|
5089
|
+
this._plot.addEventListener("pointerdown", this._onPointerMove);
|
|
5090
|
+
this._plot.addEventListener("pointermove", this._onPointerMove);
|
|
5091
|
+
this._plot.addEventListener("pointerleave", this._onLeave);
|
|
5092
|
+
this._plot.addEventListener("pointercancel", this._onCancel);
|
|
5093
|
+
this._plot.addEventListener("keydown", this._onKeyDown);
|
|
5094
|
+
this._plot.addEventListener("focus", this._onFocus);
|
|
5095
|
+
this._plot.addEventListener("blur", this._onLeave);
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
/** The class prefix of the generated parts; ColumnChart reuses this class. */
|
|
5099
|
+
get _type() {
|
|
5100
|
+
return "line-chart";
|
|
5101
|
+
}
|
|
5102
|
+
/** Whether the y axis always includes zero, as columns grow from it. */
|
|
5103
|
+
get _fromZero() {
|
|
5104
|
+
return false;
|
|
5105
|
+
}
|
|
5106
|
+
/**
|
|
5107
|
+
* Whether the rows run down the plot and the values across it, as in a
|
|
5108
|
+
* bar chart. The rows then keep their order in a right-to-left context.
|
|
5109
|
+
*/
|
|
5110
|
+
get _horizontal() {
|
|
5111
|
+
return false;
|
|
5112
|
+
}
|
|
5113
|
+
/**
|
|
5114
|
+
* Whether the rows go round a circle, as in a pie or radar chart. The
|
|
5115
|
+
* arrows then step round it, wrapping from the last row to the first.
|
|
5116
|
+
*/
|
|
5117
|
+
get _circular() {
|
|
5118
|
+
return false;
|
|
5119
|
+
}
|
|
5120
|
+
/** Whether the cursor marks each series with a point. */
|
|
5121
|
+
get _points() {
|
|
5122
|
+
return true;
|
|
5123
|
+
}
|
|
5124
|
+
get _key() {
|
|
5125
|
+
return "Expressive_LineChart";
|
|
5126
|
+
}
|
|
5127
|
+
static get defaults() {
|
|
5128
|
+
return _defaults16;
|
|
5129
|
+
}
|
|
5130
|
+
static init(els, options = {}) {
|
|
5131
|
+
return super.init(els, options, this);
|
|
5132
|
+
}
|
|
5133
|
+
static getInstance(el) {
|
|
5134
|
+
return el["Expressive_LineChart"];
|
|
5135
|
+
}
|
|
5136
|
+
destroy() {
|
|
5137
|
+
this._generated.forEach((node) => node.remove());
|
|
5138
|
+
this._generated = [];
|
|
5139
|
+
this.el[this._key] = void 0;
|
|
5140
|
+
}
|
|
5141
|
+
/**
|
|
5142
|
+
* Highlights one row: moves the cursor and fills the tooltip.
|
|
5143
|
+
* Pass -1 to hide them. Does nothing for a chart without data.
|
|
5144
|
+
*/
|
|
5145
|
+
show(index) {
|
|
5146
|
+
if (!this._tooltip || index === this.activeIndex) return;
|
|
5147
|
+
this.activeIndex = index;
|
|
5148
|
+
const hidden = index < 0 || index >= this.labels.length;
|
|
5149
|
+
this._cursor.hidden = this._tooltip.hidden = hidden;
|
|
5150
|
+
if (hidden) return;
|
|
5151
|
+
this._cursor.replaceChildren();
|
|
5152
|
+
const title = document.createElement("strong");
|
|
5153
|
+
title.textContent = this.labels[index];
|
|
5154
|
+
this._tooltip.replaceChildren(title);
|
|
5155
|
+
this.series.forEach((series, i) => {
|
|
5156
|
+
const value = series.values[index];
|
|
5157
|
+
if (value === null) return;
|
|
5158
|
+
const color = String(i % COLORS + 1);
|
|
5159
|
+
const top = series.tops[index];
|
|
5160
|
+
if (top !== null && this._points) {
|
|
5161
|
+
const dot = document.createElement("span");
|
|
5162
|
+
dot.className = series.className;
|
|
5163
|
+
dot.dataset.series = color;
|
|
5164
|
+
dot.style.top = `${series.y(top)}%`;
|
|
5165
|
+
this._cursor.append(dot);
|
|
5166
|
+
}
|
|
5167
|
+
const name = document.createElement("span");
|
|
5168
|
+
name.dataset.series = color;
|
|
5169
|
+
name.textContent = series.name;
|
|
5170
|
+
const text = document.createElement("span");
|
|
5171
|
+
text.textContent = series.text[index];
|
|
5172
|
+
this._tooltip.append(name, text);
|
|
5173
|
+
});
|
|
5174
|
+
this._place(index);
|
|
5175
|
+
}
|
|
5176
|
+
/**
|
|
5177
|
+
* Moves the filled cursor and tooltip to a row: beside its band, with the
|
|
5178
|
+
* tooltip opening back over the plot past the middle.
|
|
5179
|
+
*/
|
|
5180
|
+
_place(index) {
|
|
5181
|
+
const x = this._x(index);
|
|
5182
|
+
const side = this._horizontal ? "top" : "left";
|
|
5183
|
+
this._cursor.style[side] = this._tooltip.style[side] = `${x}%`;
|
|
5184
|
+
this._tooltip.classList.toggle("end", x > 50);
|
|
5185
|
+
}
|
|
5186
|
+
_read() {
|
|
5187
|
+
const table = this.el.querySelector(":scope > table");
|
|
5188
|
+
if (!table) return;
|
|
5189
|
+
const headerRow = table.tHead?.rows[0] ?? table.rows[0];
|
|
5190
|
+
const header = Array.from(headerRow?.cells ?? []);
|
|
5191
|
+
const rows = Array.from(table.tBodies).flatMap((body) => Array.from(body.rows)).filter((row) => row !== headerRow);
|
|
5192
|
+
[this._header, this._rows] = [header, rows];
|
|
5193
|
+
this.labels = rows.map((row) => row.cells[0]?.textContent.trim() ?? "");
|
|
5194
|
+
this.series = header.slice(1).map((cell, i) => {
|
|
5195
|
+
const values = rows.map((row) => cellValue(row.cells[i + 1]));
|
|
5196
|
+
return {
|
|
5197
|
+
name: cell.textContent.trim(),
|
|
5198
|
+
className: cell.className,
|
|
5199
|
+
text: rows.map((row) => row.cells[i + 1]?.textContent.trim() ?? ""),
|
|
5200
|
+
values,
|
|
5201
|
+
tops: values,
|
|
5202
|
+
y: void 0
|
|
5203
|
+
};
|
|
5204
|
+
});
|
|
5205
|
+
const stacked = this.el.classList.contains("stacked");
|
|
5206
|
+
if (stacked) {
|
|
5207
|
+
const totals = this.labels.map(() => 0);
|
|
5208
|
+
for (const series of this.series) {
|
|
5209
|
+
series.base = [...totals];
|
|
5210
|
+
series.tops = series.values.map((value, i) => totals[i] = value === null || totals[i] === null ? null : totals[i] + value);
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
if (this.series.every((series) => series.tops.every((value) => value === null))) this.series = [];
|
|
5214
|
+
const option = (name) => this._number(name) ?? this.options[name];
|
|
5215
|
+
this._y = this._scale(this.series, option("min"), option("max"));
|
|
5216
|
+
}
|
|
5217
|
+
/** A number from one of the chart's data attributes, or null. */
|
|
5218
|
+
_number(name) {
|
|
5219
|
+
const value = parseFloat(this.el.dataset[name] ?? "");
|
|
5220
|
+
return Number.isFinite(value) ? value : null;
|
|
5221
|
+
}
|
|
5222
|
+
/**
|
|
5223
|
+
* Fits a y scale to the series' tops, between `min` and `max` where they
|
|
5224
|
+
* are set, and gives it to each series.
|
|
5225
|
+
*/
|
|
5226
|
+
_scale(series, min, max, fromZero = this._fromZero) {
|
|
5227
|
+
const stacked = this.el.classList.contains("stacked");
|
|
5228
|
+
let [low, high] = [Infinity, -Infinity];
|
|
5229
|
+
for (const { tops } of series) {
|
|
5230
|
+
for (const value of tops) {
|
|
5231
|
+
if (value === null) continue;
|
|
5232
|
+
low = Math.min(low, value);
|
|
5233
|
+
high = Math.max(high, value);
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
if (low > high) low = high = 0;
|
|
5237
|
+
if (fromZero) {
|
|
5238
|
+
const [bottom, top] = [Math.min(0, low), Math.max(0, high)];
|
|
5239
|
+
const room = (top - (min ?? bottom)) * 0.1 || 1;
|
|
5240
|
+
min ?? (min = bottom < 0 ? bottom - room : 0);
|
|
5241
|
+
max ?? (max = top + room);
|
|
5242
|
+
} else {
|
|
5243
|
+
const pad = (high - low) * 0.1 || 1;
|
|
5244
|
+
min ?? (min = stacked ? Math.min(0, low) : low - pad);
|
|
5245
|
+
max ?? (max = high + (stacked ? (high - min) * 0.1 || 1 : pad));
|
|
5246
|
+
}
|
|
5247
|
+
const y = (value) => 100 - (value - min) / (max - min || 1) * 100;
|
|
5248
|
+
for (const each of series) each.y = y;
|
|
5249
|
+
return y;
|
|
5250
|
+
}
|
|
5251
|
+
/** Each row sits in the middle of an equal band, beside its label. */
|
|
5252
|
+
_x(index) {
|
|
5253
|
+
const x = (index + 0.5) / this.labels.length * 100;
|
|
5254
|
+
return this._reversed ? 100 - x : x;
|
|
5255
|
+
}
|
|
5256
|
+
_render() {
|
|
5257
|
+
const type = this._type;
|
|
5258
|
+
this._plot = document.createElement("div");
|
|
5259
|
+
this._plot.className = `${type}-plot`;
|
|
5260
|
+
const caption = this.el.querySelector(":scope > figcaption")?.textContent.trim();
|
|
5261
|
+
this._plot.setAttribute("role", "group");
|
|
5262
|
+
this._plot.setAttribute("aria-roledescription", type.replace(/-/g, " "));
|
|
5263
|
+
if (caption) this._plot.setAttribute("aria-label", caption);
|
|
5264
|
+
this._cursor = document.createElement("div");
|
|
5265
|
+
this._cursor.className = `${type}-cursor`;
|
|
5266
|
+
this._cursor.hidden = true;
|
|
5267
|
+
this._tooltip = document.createElement("div");
|
|
5268
|
+
this._tooltip.className = `${type}-tooltip`;
|
|
5269
|
+
this._tooltip.hidden = true;
|
|
5270
|
+
this._tooltip.setAttribute("aria-live", "polite");
|
|
5271
|
+
this._plot.append(this._draw(), this._cursor, this._tooltip);
|
|
5272
|
+
this._generated = [this._plot, ...this._guides()];
|
|
5273
|
+
this.el.append(...this._generated);
|
|
5274
|
+
}
|
|
5275
|
+
/**
|
|
5276
|
+
* The <tfoot> row as a total to show over the plot, its value over its
|
|
5277
|
+
* label, or null without one.
|
|
5278
|
+
*/
|
|
5279
|
+
_total(className) {
|
|
5280
|
+
const footer = this.el.querySelector(":scope > table")?.tFoot?.rows[0];
|
|
5281
|
+
if (!footer) return null;
|
|
5282
|
+
const total = document.createElement("div");
|
|
5283
|
+
total.className = className;
|
|
5284
|
+
const [label, value] = [document.createElement("span"), document.createElement("strong")];
|
|
5285
|
+
label.textContent = footer.cells[0]?.textContent.trim() ?? "";
|
|
5286
|
+
value.textContent = footer.cells[1]?.textContent.trim() ?? "";
|
|
5287
|
+
total.append(value, label);
|
|
5288
|
+
return total;
|
|
5289
|
+
}
|
|
5290
|
+
/** The parts below the plot: the row labels and the legend. */
|
|
5291
|
+
_guides() {
|
|
5292
|
+
const labels = document.createElement("ol");
|
|
5293
|
+
labels.className = `${this._type}-labels`;
|
|
5294
|
+
labels.setAttribute("aria-hidden", "true");
|
|
5295
|
+
for (const text of this.labels) {
|
|
5296
|
+
const li = document.createElement("li");
|
|
5297
|
+
li.textContent = text;
|
|
5298
|
+
labels.append(li);
|
|
5299
|
+
}
|
|
5300
|
+
return [labels, ...this._legend()];
|
|
5301
|
+
}
|
|
5302
|
+
/** The series legend, for more than one series. */
|
|
5303
|
+
_legend() {
|
|
5304
|
+
if (this.series.length < 2) return [];
|
|
5305
|
+
const legend = document.createElement("ul");
|
|
5306
|
+
legend.className = `${this._type}-legend`;
|
|
5307
|
+
legend.setAttribute("aria-hidden", "true");
|
|
5308
|
+
legend.append(...this.series.map((series, i) => {
|
|
5309
|
+
const li = document.createElement("li");
|
|
5310
|
+
li.dataset.series = String(i % COLORS + 1);
|
|
5311
|
+
li.textContent = series.name;
|
|
5312
|
+
return li;
|
|
5313
|
+
}));
|
|
5314
|
+
return [legend];
|
|
5315
|
+
}
|
|
5316
|
+
/** Draws the series, which fill the plot. */
|
|
5317
|
+
_draw(drawn = this.series) {
|
|
5318
|
+
const svg = document.createElementNS(SVG, "svg");
|
|
5319
|
+
svg.setAttribute("viewBox", "0 0 100 100");
|
|
5320
|
+
svg.setAttribute("preserveAspectRatio", "none");
|
|
5321
|
+
svg.setAttribute("aria-hidden", "true");
|
|
5322
|
+
svg.setAttribute("focusable", "false");
|
|
5323
|
+
for (const series of drawn) {
|
|
5324
|
+
const group = document.createElementNS(SVG, "g");
|
|
5325
|
+
group.setAttribute("class", `${this._type}-series ${series.className}`.trim());
|
|
5326
|
+
group.dataset.series = String(this.series.indexOf(series) % COLORS + 1);
|
|
5327
|
+
const runs = [[]];
|
|
5328
|
+
series.tops.forEach((value, index) => {
|
|
5329
|
+
if (value === null) runs.push([]);
|
|
5330
|
+
else runs[runs.length - 1].push(index);
|
|
5331
|
+
});
|
|
5332
|
+
const filled = runs.filter((run) => run.length);
|
|
5333
|
+
const points = (run, values) => run.map((index) => [this._x(index), series.y(values[index])]);
|
|
5334
|
+
const tops = filled.map((run) => monotonePath(points(run, series.tops)));
|
|
5335
|
+
const id = `line-chart-gradient-${++_gradientId}`;
|
|
5336
|
+
const gradient = document.createElementNS(SVG, "linearGradient");
|
|
5337
|
+
gradient.id = id;
|
|
5338
|
+
gradient.setAttribute("x2", "0");
|
|
5339
|
+
gradient.setAttribute("y2", "1");
|
|
5340
|
+
gradient.append(document.createElementNS(SVG, "stop"), document.createElementNS(SVG, "stop"));
|
|
5341
|
+
gradient.lastElementChild.setAttribute("offset", "1");
|
|
5342
|
+
const area = document.createElementNS(SVG, "path");
|
|
5343
|
+
area.setAttribute("class", `${this._type}-area`);
|
|
5344
|
+
area.setAttribute("fill", `url(#${id})`);
|
|
5345
|
+
area.setAttribute("d", filled.map((run, r) => {
|
|
5346
|
+
if (!series.base) return `${tops[r]}V100H${this._x(run[0])}Z`;
|
|
5347
|
+
const base = points(run, series.base).map(([x, y]) => [x, Math.min(100, y)]);
|
|
5348
|
+
return `${tops[r]}L${monotonePath(base.reverse()).slice(1)}Z`;
|
|
5349
|
+
}).join(""));
|
|
5350
|
+
const line = document.createElementNS(SVG, "path");
|
|
5351
|
+
line.setAttribute("class", `${this._type}-line`);
|
|
5352
|
+
line.setAttribute("d", tops.join(""));
|
|
5353
|
+
group.append(gradient, area, line);
|
|
5354
|
+
svg.append(group);
|
|
5355
|
+
}
|
|
5356
|
+
return svg;
|
|
5357
|
+
}
|
|
5358
|
+
/** The row under the pointer, or -1. */
|
|
5359
|
+
_indexAt(e) {
|
|
5360
|
+
const box = this._plot.getBoundingClientRect();
|
|
5361
|
+
const fraction = this._horizontal ? (e.clientY - box.top) / box.height : (e.clientX - box.left) / box.width;
|
|
5362
|
+
const index = Math.floor((this._reversed ? 1 - fraction : fraction) * this.labels.length);
|
|
5363
|
+
return Math.min(this.labels.length - 1, Math.max(0, index));
|
|
5364
|
+
}
|
|
5365
|
+
/**
|
|
5366
|
+
* For a circular chart, the pointer's position from the plot's middle: a
|
|
5367
|
+
* fraction of a turn clockwise from the top, and its distance as a share
|
|
5368
|
+
* of the plot's width.
|
|
5369
|
+
*/
|
|
5370
|
+
_polarAt(e) {
|
|
5371
|
+
const box = this._plot.getBoundingClientRect();
|
|
5372
|
+
const [x, y] = [e.clientX - box.left - box.width / 2, e.clientY - box.top - box.height / 2];
|
|
5373
|
+
return [(Math.atan2(x, -y) / (2 * Math.PI) + 1) % 1, Math.hypot(x, y) / box.width];
|
|
5374
|
+
}
|
|
5375
|
+
/** The keys that move to the next row and to the one before. */
|
|
5376
|
+
get _arrows() {
|
|
5377
|
+
if (this._circular) return [["ArrowRight", "ArrowDown"], ["ArrowLeft", "ArrowUp"]];
|
|
5378
|
+
if (this._horizontal) return [["ArrowDown"], ["ArrowUp"]];
|
|
5379
|
+
return this._reversed ? [["ArrowLeft"], ["ArrowRight"]] : [["ArrowRight"], ["ArrowLeft"]];
|
|
5380
|
+
}
|
|
5381
|
+
/** The index a key moves to, or undefined for a key the chart ignores. */
|
|
5382
|
+
_keyTarget(key) {
|
|
5383
|
+
const last = this.labels.length - 1;
|
|
5384
|
+
const [forward, back] = this._arrows;
|
|
5385
|
+
const step = (by) => {
|
|
5386
|
+
if (!this._circular) return Math.min(last, Math.max(0, this.activeIndex + by));
|
|
5387
|
+
if (this.activeIndex < 0) return by > 0 ? 0 : last;
|
|
5388
|
+
return (this.activeIndex + by + last + 1) % (last + 1);
|
|
5389
|
+
};
|
|
5390
|
+
if (key === "Escape") return this.activeIndex < 0 ? void 0 : -1;
|
|
5391
|
+
return forward.includes(key) ? step(1) : back.includes(key) ? step(-1) : { Home: 0, End: last }[key];
|
|
5392
|
+
}
|
|
5393
|
+
};
|
|
5394
|
+
|
|
5395
|
+
// src/ts/components/columnChart.ts
|
|
5396
|
+
var ColumnChart = class extends LineChart {
|
|
5397
|
+
get _type() {
|
|
5398
|
+
return "column-chart";
|
|
5399
|
+
}
|
|
5400
|
+
get _key() {
|
|
5401
|
+
return "Expressive_ColumnChart";
|
|
5402
|
+
}
|
|
5403
|
+
get _fromZero() {
|
|
5404
|
+
return true;
|
|
5405
|
+
}
|
|
5406
|
+
get _points() {
|
|
5407
|
+
return false;
|
|
5408
|
+
}
|
|
5409
|
+
static init(els, options = {}) {
|
|
5410
|
+
return super.init(els, options);
|
|
5411
|
+
}
|
|
5412
|
+
static getInstance(el) {
|
|
5413
|
+
return el["Expressive_ColumnChart"];
|
|
5414
|
+
}
|
|
5415
|
+
_draw(drawn = this.series) {
|
|
5416
|
+
const columns = document.createElement("div");
|
|
5417
|
+
columns.className = "column-chart-columns";
|
|
5418
|
+
columns.setAttribute("aria-hidden", "true");
|
|
5419
|
+
const clamp = (value, y = drawn[0]?.y ?? this._y) => Math.min(100, Math.max(0, y(value)));
|
|
5420
|
+
columns.style.setProperty("--zero", `${clamp(0)}%`);
|
|
5421
|
+
this._plot.style.setProperty("--rows", String(this.labels.length));
|
|
5422
|
+
this.labels.forEach((_, index) => {
|
|
5423
|
+
const row = document.createElement("div");
|
|
5424
|
+
drawn.forEach((series) => {
|
|
5425
|
+
const to = series.tops[index];
|
|
5426
|
+
const from = series.base?.[index] ?? 0;
|
|
5427
|
+
if (series.base && (to === null || from === null)) return;
|
|
5428
|
+
const column = document.createElement("span");
|
|
5429
|
+
if (to !== null) {
|
|
5430
|
+
column.className = series.className;
|
|
5431
|
+
column.dataset.series = String(this.series.indexOf(series) % COLORS + 1);
|
|
5432
|
+
column.classList.toggle("negative", !series.base && to < from);
|
|
5433
|
+
column.style.setProperty("--top", `${clamp(Math.max(from, to), series.y)}%`);
|
|
5434
|
+
column.style.setProperty("--bottom", `${100 - clamp(Math.min(from, to), series.y)}%`);
|
|
5435
|
+
if (this._horizontal) column.dataset.text = series.text[index];
|
|
5436
|
+
}
|
|
5437
|
+
row.append(column);
|
|
5438
|
+
});
|
|
5439
|
+
columns.append(row);
|
|
5440
|
+
});
|
|
5441
|
+
return columns;
|
|
5442
|
+
}
|
|
5443
|
+
};
|
|
5444
|
+
|
|
5445
|
+
// src/ts/components/barChart.ts
|
|
5446
|
+
var BarChart = class extends ColumnChart {
|
|
5447
|
+
get _type() {
|
|
5448
|
+
return "bar-chart";
|
|
5449
|
+
}
|
|
5450
|
+
get _key() {
|
|
5451
|
+
return "Expressive_BarChart";
|
|
5452
|
+
}
|
|
5453
|
+
get _horizontal() {
|
|
5454
|
+
return true;
|
|
5455
|
+
}
|
|
5456
|
+
static init(els, options = {}) {
|
|
5457
|
+
return super.init(els, options);
|
|
5458
|
+
}
|
|
5459
|
+
static getInstance(el) {
|
|
5460
|
+
return el["Expressive_BarChart"];
|
|
5461
|
+
}
|
|
5462
|
+
/**
|
|
5463
|
+
* The column chart's spans; CSS reads their --top and --bottom across the
|
|
5464
|
+
* plot. With .values, a copy of the bars without their clipping holds each
|
|
5465
|
+
* cell's text, so text longer than a short bar can run past its end.
|
|
5466
|
+
*/
|
|
5467
|
+
_draw() {
|
|
5468
|
+
const bars = super._draw();
|
|
5469
|
+
bars.className = "bar-chart-bars";
|
|
5470
|
+
bars.classList.toggle("rtl", getComputedStyle(this.el).direction === "rtl");
|
|
5471
|
+
if (!this.el.classList.contains("values")) return bars;
|
|
5472
|
+
const values = bars.cloneNode(true);
|
|
5473
|
+
values.classList.replace("bar-chart-bars", "bar-chart-values");
|
|
5474
|
+
values.querySelectorAll("span[data-text]").forEach((span) => {
|
|
5475
|
+
span.textContent = span.dataset.text;
|
|
5476
|
+
});
|
|
5477
|
+
const layers = document.createElement("div");
|
|
5478
|
+
layers.className = "bar-chart-layers";
|
|
5479
|
+
layers.append(bars, values);
|
|
5480
|
+
return layers;
|
|
5481
|
+
}
|
|
5482
|
+
};
|
|
5483
|
+
|
|
5484
|
+
// src/ts/components/pieChart.ts
|
|
5485
|
+
var COLORS2 = 6;
|
|
5486
|
+
var RADIUS = 46;
|
|
5487
|
+
var GROWN = 1.06;
|
|
5488
|
+
var LABEL_SHARE = 0.05;
|
|
5489
|
+
var _maskId = 0;
|
|
5490
|
+
var PieChart = class extends LineChart {
|
|
5491
|
+
get _type() {
|
|
5492
|
+
return "pie-chart";
|
|
5493
|
+
}
|
|
5494
|
+
get _key() {
|
|
5495
|
+
return "Expressive_PieChart";
|
|
5496
|
+
}
|
|
5497
|
+
get _points() {
|
|
5498
|
+
return false;
|
|
5499
|
+
}
|
|
5500
|
+
get _circular() {
|
|
5501
|
+
return true;
|
|
5502
|
+
}
|
|
5503
|
+
static init(els, options = {}) {
|
|
5504
|
+
return super.init(els, options);
|
|
5505
|
+
}
|
|
5506
|
+
static getInstance(el) {
|
|
5507
|
+
return el["Expressive_PieChart"];
|
|
5508
|
+
}
|
|
5509
|
+
show(index) {
|
|
5510
|
+
if (!this._tooltip || index === this.activeIndex) return;
|
|
5511
|
+
super.show(index);
|
|
5512
|
+
this._slices.forEach((slice, i) => slice?.classList.toggle("active", i === index));
|
|
5513
|
+
if (this._tooltip.hidden) return;
|
|
5514
|
+
const color = this._colors[index];
|
|
5515
|
+
this._tooltip.querySelectorAll("[data-series]").forEach((span) => {
|
|
5516
|
+
if (color) span.dataset.series = color;
|
|
5517
|
+
else delete span.dataset.series;
|
|
5518
|
+
});
|
|
5519
|
+
const share = this._share(index);
|
|
5520
|
+
const value = this._tooltip.querySelector("span:nth-of-type(2)");
|
|
5521
|
+
if (share && value) value.textContent += ` (${share})`;
|
|
5522
|
+
}
|
|
5523
|
+
/** CSS places the tooltip on the slice's middle from --turn. */
|
|
5524
|
+
_place(index) {
|
|
5525
|
+
const middle = (this._turns[index][0] + this._turns[index][1]) / 2;
|
|
5526
|
+
this._tooltip.style.setProperty("--turn", String(middle));
|
|
5527
|
+
this._tooltip.classList.toggle("end", middle < 0.5);
|
|
5528
|
+
}
|
|
5529
|
+
/** The row's share of the whole as a percentage, or '' for none. */
|
|
5530
|
+
_share(index) {
|
|
5531
|
+
var _a;
|
|
5532
|
+
const [start, end] = this._turns[index];
|
|
5533
|
+
if (end <= start) return "";
|
|
5534
|
+
const share = end - start;
|
|
5535
|
+
const digits = share < 0.1 ? 1 : 0;
|
|
5536
|
+
(_a = this._percents ?? (this._percents = []))[digits] ?? (_a[digits] = this._percent(digits));
|
|
5537
|
+
return this._percents[digits].format(share);
|
|
5538
|
+
}
|
|
5539
|
+
_percent(digits) {
|
|
5540
|
+
const options = { style: "percent", maximumFractionDigits: digits };
|
|
5541
|
+
try {
|
|
5542
|
+
return new Intl.NumberFormat(this.el.closest("[lang]")?.lang || void 0, options);
|
|
5543
|
+
} catch {
|
|
5544
|
+
return new Intl.NumberFormat(void 0, options);
|
|
5545
|
+
}
|
|
5546
|
+
}
|
|
5547
|
+
/** The legend: every row with its color, its cell and its share. */
|
|
5548
|
+
_guides() {
|
|
5549
|
+
this._percents = [];
|
|
5550
|
+
const legend = document.createElement("ul");
|
|
5551
|
+
legend.className = "pie-chart-legend";
|
|
5552
|
+
legend.setAttribute("aria-hidden", "true");
|
|
5553
|
+
const text = this.series[0].text;
|
|
5554
|
+
legend.append(...this.labels.map((label, i) => {
|
|
5555
|
+
const li = document.createElement("li");
|
|
5556
|
+
if (this._colors[i]) li.dataset.series = this._colors[i];
|
|
5557
|
+
const [name, value, share] = [label, text[i], this._share(i)].map((content) => {
|
|
5558
|
+
const span = document.createElement("span");
|
|
5559
|
+
span.textContent = content;
|
|
5560
|
+
return span;
|
|
5561
|
+
});
|
|
5562
|
+
li.append(name, value, share);
|
|
5563
|
+
return li;
|
|
5564
|
+
}));
|
|
5565
|
+
return [legend];
|
|
5566
|
+
}
|
|
5567
|
+
/** The row under the pointer by its angle from the middle, or -1 off the slices. */
|
|
5568
|
+
_indexAt(e) {
|
|
5569
|
+
const [turn, share] = this._polarAt(e);
|
|
5570
|
+
const distance = share * 100;
|
|
5571
|
+
const hole = this.el.classList.contains("donut") ? parseFloat(getComputedStyle(this.el).getPropertyValue("--md-comp-pie-chart-hole")) || 0 : 0;
|
|
5572
|
+
if (distance < RADIUS * hole) return -1;
|
|
5573
|
+
const index = this._turns.findIndex(([start, end]) => start <= turn && turn < end);
|
|
5574
|
+
return distance > RADIUS * (index === this.activeIndex ? GROWN : 1) ? -1 : index;
|
|
5575
|
+
}
|
|
5576
|
+
/**
|
|
5577
|
+
* An SVG of slices in a -50 to 50 box, under a mask that cuts the gaps
|
|
5578
|
+
* between them and the donut's hole, which CSS sizes. The values and the
|
|
5579
|
+
* total are HTML over it.
|
|
5580
|
+
*/
|
|
5581
|
+
_draw() {
|
|
5582
|
+
const values = this.series[0].values.map((value) => Math.max(0, value ?? 0));
|
|
5583
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
5584
|
+
let turn = 0;
|
|
5585
|
+
this._turns = values.map((value) => [turn, turn += total ? value / total : 0]);
|
|
5586
|
+
const drawn = this._turns.filter(([start, end]) => end > start).length;
|
|
5587
|
+
let slice = 0;
|
|
5588
|
+
this._colors = this._turns.map(([start, end]) => {
|
|
5589
|
+
if (end <= start) return void 0;
|
|
5590
|
+
const index = slice++;
|
|
5591
|
+
return String(index === drawn - 1 && index > 0 && index % COLORS2 === 0 ? 2 : index % COLORS2 + 1);
|
|
5592
|
+
});
|
|
5593
|
+
const at = (t, r = RADIUS) => {
|
|
5594
|
+
const angle = t * 2 * Math.PI;
|
|
5595
|
+
return `${(r * Math.sin(angle)).toFixed(3)},${(-r * Math.cos(angle)).toFixed(3)}`;
|
|
5596
|
+
};
|
|
5597
|
+
const pie = document.createElement("div");
|
|
5598
|
+
pie.className = "pie-chart-pie";
|
|
5599
|
+
pie.setAttribute("aria-hidden", "true");
|
|
5600
|
+
const svg = svgNode("svg", { viewBox: "-50 -50 100 100", focusable: "false" });
|
|
5601
|
+
const id = `pie-chart-mask-${++_maskId}`;
|
|
5602
|
+
const area = { x: "-50", y: "-50", width: "100", height: "100" };
|
|
5603
|
+
const mask = svgNode("mask", { id, maskUnits: "userSpaceOnUse", ...area });
|
|
5604
|
+
const edges = this._turns.filter(([start, end]) => end > start);
|
|
5605
|
+
mask.append(
|
|
5606
|
+
svgNode("rect", { ...area, fill: "white" }),
|
|
5607
|
+
svgNode("path", {
|
|
5608
|
+
class: "pie-chart-gaps",
|
|
5609
|
+
stroke: "black",
|
|
5610
|
+
d: edges.length > 1 ? edges.map(([start]) => `M0,0L${at(start, 50)}`).join("") : ""
|
|
5611
|
+
}),
|
|
5612
|
+
svgNode("circle", { class: "pie-chart-hole", r: String(RADIUS), fill: "black" })
|
|
5613
|
+
);
|
|
5614
|
+
const slices = svgNode("g", { mask: `url(#${id})` });
|
|
5615
|
+
const arc2 = `A${RADIUS},${RADIUS} 0`;
|
|
5616
|
+
this._slices = this._turns.map(([start, end], i) => {
|
|
5617
|
+
if (end <= start) return null;
|
|
5618
|
+
const slice2 = svgNode("path", {
|
|
5619
|
+
class: "pie-chart-slice",
|
|
5620
|
+
"data-series": this._colors[i],
|
|
5621
|
+
// A whole circle is two half arcs; one arc cannot end where it starts.
|
|
5622
|
+
d: end - start >= 1 ? `M0,${-RADIUS}${arc2} 1 1 0,${RADIUS}${arc2} 1 1 0,${-RADIUS}Z` : `M0,0L${at(start)}${arc2} ${end - start > 0.5 ? 1 : 0} 1 ${at(end)}Z`
|
|
5623
|
+
});
|
|
5624
|
+
slices.append(slice2);
|
|
5625
|
+
return slice2;
|
|
5626
|
+
});
|
|
5627
|
+
svg.append(mask, slices);
|
|
5628
|
+
pie.append(svg);
|
|
5629
|
+
if (this.el.classList.contains("values")) {
|
|
5630
|
+
const labels = document.createElement("div");
|
|
5631
|
+
labels.className = "pie-chart-values";
|
|
5632
|
+
this._turns.forEach(([start, end], i) => {
|
|
5633
|
+
if (end - start < LABEL_SHARE) return;
|
|
5634
|
+
const label = document.createElement("span");
|
|
5635
|
+
label.dataset.series = this._colors[i];
|
|
5636
|
+
label.style.setProperty("--turn", String((start + end) / 2));
|
|
5637
|
+
label.textContent = this._share(i);
|
|
5638
|
+
labels.append(label);
|
|
5639
|
+
});
|
|
5640
|
+
pie.append(labels);
|
|
5641
|
+
}
|
|
5642
|
+
const middle = this._total("pie-chart-total");
|
|
5643
|
+
if (middle) pie.append(middle);
|
|
5644
|
+
return pie;
|
|
5645
|
+
}
|
|
5646
|
+
};
|
|
5647
|
+
|
|
5648
|
+
// src/ts/components/heatmapChart.ts
|
|
5649
|
+
var HeatmapChart = class extends LineChart {
|
|
5650
|
+
get _type() {
|
|
5651
|
+
return "heatmap-chart";
|
|
5652
|
+
}
|
|
5653
|
+
get _key() {
|
|
5654
|
+
return "Expressive_HeatmapChart";
|
|
5655
|
+
}
|
|
5656
|
+
static init(els, options = {}) {
|
|
5657
|
+
return super.init(els, options);
|
|
5658
|
+
}
|
|
5659
|
+
static getInstance(el) {
|
|
5660
|
+
return el["Expressive_HeatmapChart"];
|
|
5661
|
+
}
|
|
5662
|
+
/** Highlights one cell, numbered row by row. Pass -1 to hide the tooltip. */
|
|
5663
|
+
show(index) {
|
|
5664
|
+
if (!this._tooltip || index === this.activeIndex) return;
|
|
5665
|
+
const cell = this._cells[index];
|
|
5666
|
+
const [left, top, width, plotWidth] = cell ? [cell.offsetLeft, cell.offsetTop, cell.offsetWidth, this._plot.clientWidth] : [];
|
|
5667
|
+
this._cells[this.activeIndex]?.classList.remove("active");
|
|
5668
|
+
this.activeIndex = index;
|
|
5669
|
+
this._tooltip.hidden = !cell;
|
|
5670
|
+
if (!cell) return;
|
|
5671
|
+
cell.classList.add("active");
|
|
5672
|
+
const columns = this.series.length;
|
|
5673
|
+
const [row, series] = [Math.floor(index / columns), this.series[index % columns]];
|
|
5674
|
+
const title = document.createElement("strong");
|
|
5675
|
+
title.textContent = this.labels[row];
|
|
5676
|
+
const [name, text] = [document.createElement("span"), document.createElement("span")];
|
|
5677
|
+
name.textContent = series.name;
|
|
5678
|
+
if (this._levels[index] === null) name.className = "gap";
|
|
5679
|
+
else name.style.setProperty("--level", String(this._levels[index]));
|
|
5680
|
+
text.textContent = series.text[row];
|
|
5681
|
+
this._tooltip.replaceChildren(title, name, text);
|
|
5682
|
+
const end = left + width / 2 > plotWidth / 2;
|
|
5683
|
+
this._tooltip.classList.toggle("end", end);
|
|
5684
|
+
this._tooltip.style.left = `${left + (end ? 0 : width)}px`;
|
|
5685
|
+
this._tooltip.style.top = `${top}px`;
|
|
5686
|
+
}
|
|
5687
|
+
/** Arrows move the way they point, so left and right swap in RTL. */
|
|
5688
|
+
_keyTarget(key) {
|
|
5689
|
+
if (key === "Escape") return this.activeIndex < 0 ? void 0 : -1;
|
|
5690
|
+
const [columns, last] = [this.series.length, this.labels.length - 1];
|
|
5691
|
+
const index = Math.max(0, this.activeIndex);
|
|
5692
|
+
const [row, column] = [Math.floor(index / columns), index % columns];
|
|
5693
|
+
const [next, back] = this._reversed ? ["ArrowLeft", "ArrowRight"] : ["ArrowRight", "ArrowLeft"];
|
|
5694
|
+
const to = {
|
|
5695
|
+
[next]: [row, column + 1],
|
|
5696
|
+
[back]: [row, column - 1],
|
|
5697
|
+
ArrowDown: [row + 1, column],
|
|
5698
|
+
ArrowUp: [row - 1, column],
|
|
5699
|
+
Home: [row, 0],
|
|
5700
|
+
End: [row, columns - 1]
|
|
5701
|
+
}[key];
|
|
5702
|
+
if (!to) return void 0;
|
|
5703
|
+
if (this.activeIndex < 0) return 0;
|
|
5704
|
+
const clamp = (value, max) => Math.min(max, Math.max(0, value));
|
|
5705
|
+
return clamp(to[0], last) * columns + clamp(to[1], columns - 1);
|
|
5706
|
+
}
|
|
5707
|
+
/**
|
|
5708
|
+
* The cell under the pointer by its position, or -1 off the grid. A touch
|
|
5709
|
+
* keeps its first target while it drags, so the target cannot be used. The
|
|
5710
|
+
* cells and gaps are even, so each band's edge falls in a gap and a gap
|
|
5711
|
+
* counts as the nearer cell.
|
|
5712
|
+
*/
|
|
5713
|
+
_indexAt(e) {
|
|
5714
|
+
const [columns, rows] = [this.series.length, this.labels.length];
|
|
5715
|
+
const first = this._cells[0].getBoundingClientRect();
|
|
5716
|
+
const last = this._cells[this._cells.length - 1].getBoundingClientRect();
|
|
5717
|
+
const [left, right] = [Math.min(first.left, last.left), Math.max(first.right, last.right)];
|
|
5718
|
+
const [x, y] = [e.clientX, e.clientY];
|
|
5719
|
+
if (x < left || x > right || y < first.top || y > last.bottom) return -1;
|
|
5720
|
+
const band = (offset, size, count) => Math.min(count - 1, Math.floor(offset / (size || 1) * count));
|
|
5721
|
+
const column = band(x - left, right - left, columns);
|
|
5722
|
+
return band(y - first.top, last.bottom - first.top, rows) * columns + (this._reversed ? columns - 1 - column : column);
|
|
5723
|
+
}
|
|
5724
|
+
/**
|
|
5725
|
+
* The legend: a gradient from the lowest cell's shade to the highest's,
|
|
5726
|
+
* labelled with their text. A fixed scale wider than the data shows only
|
|
5727
|
+
* the part the data uses.
|
|
5728
|
+
*/
|
|
5729
|
+
_guides() {
|
|
5730
|
+
const legend = document.createElement("div");
|
|
5731
|
+
legend.className = "heatmap-chart-legend";
|
|
5732
|
+
legend.setAttribute("aria-hidden", "true");
|
|
5733
|
+
const [low, scale, high] = ["span", "span", "span"].map((tag) => document.createElement(tag));
|
|
5734
|
+
[low.textContent, high.textContent] = this._ends;
|
|
5735
|
+
scale.className = "heatmap-chart-scale";
|
|
5736
|
+
if (this._reversed) scale.style.setProperty("--heatmap-chart-to", "left");
|
|
5737
|
+
scale.style.setProperty("--low", String(this._endLevels[0]));
|
|
5738
|
+
scale.style.setProperty("--high", String(this._endLevels[1]));
|
|
5739
|
+
legend.append(low, scale, high);
|
|
5740
|
+
return [legend];
|
|
5741
|
+
}
|
|
5742
|
+
/**
|
|
5743
|
+
* A grid of the row headers and cells, row by row, with the column headers
|
|
5744
|
+
* below. Each cell's --level shades it in CSS.
|
|
5745
|
+
*/
|
|
5746
|
+
_draw() {
|
|
5747
|
+
const columns = this.series.length;
|
|
5748
|
+
let [low, high] = [Infinity, -Infinity];
|
|
5749
|
+
this._ends = ["", ""];
|
|
5750
|
+
this.series.forEach((series) => series.values.forEach((value, row) => {
|
|
5751
|
+
if (value === null) return;
|
|
5752
|
+
if (value < low) [low, this._ends[0]] = [value, series.text[row]];
|
|
5753
|
+
if (value > high) [high, this._ends[1]] = [value, series.text[row]];
|
|
5754
|
+
}));
|
|
5755
|
+
const option = (name) => {
|
|
5756
|
+
const value = parseFloat(this.el.dataset[name] ?? "");
|
|
5757
|
+
return Number.isFinite(value) ? value : this.options[name];
|
|
5758
|
+
};
|
|
5759
|
+
const [min, max] = [option("min") ?? low, option("max") ?? high];
|
|
5760
|
+
const shade = (value) => Math.min(1, Math.max(0, (value - min) / (max - min || 1)));
|
|
5761
|
+
this._endLevels = [shade(low), shade(high)];
|
|
5762
|
+
const grid = document.createElement("div");
|
|
5763
|
+
grid.className = "heatmap-chart-grid";
|
|
5764
|
+
grid.setAttribute("aria-hidden", "true");
|
|
5765
|
+
grid.style.setProperty("--columns", String(columns));
|
|
5766
|
+
grid.style.setProperty("--rows", String(this.labels.length));
|
|
5767
|
+
const span = (className, text = "") => {
|
|
5768
|
+
const element = document.createElement("span");
|
|
5769
|
+
element.className = className;
|
|
5770
|
+
element.textContent = text;
|
|
5771
|
+
return element;
|
|
5772
|
+
};
|
|
5773
|
+
const label = (cell) => cell?.dataset.label ?? cell?.textContent.trim() ?? "";
|
|
5774
|
+
const values = this.el.classList.contains("values");
|
|
5775
|
+
this._cells = [];
|
|
5776
|
+
this._levels = [];
|
|
5777
|
+
this.labels.forEach((_, row) => {
|
|
5778
|
+
grid.append(span("heatmap-chart-row", label(this._rows[row].cells[0])));
|
|
5779
|
+
this.series.forEach((series) => {
|
|
5780
|
+
const value = series.values[row];
|
|
5781
|
+
const level = value === null ? null : shade(value);
|
|
5782
|
+
const cell = span("heatmap-chart-cell", values ? series.text[row] : "");
|
|
5783
|
+
if (level === null) cell.classList.add("gap");
|
|
5784
|
+
else cell.style.setProperty("--level", String(level));
|
|
5785
|
+
this._cells.push(cell);
|
|
5786
|
+
this._levels.push(level);
|
|
5787
|
+
grid.append(cell);
|
|
5788
|
+
});
|
|
5789
|
+
});
|
|
5790
|
+
const labels = this._header.slice(1, columns + 1).map(label);
|
|
5791
|
+
labels.forEach((text, column) => {
|
|
5792
|
+
if (!text) return;
|
|
5793
|
+
let end = column + 1;
|
|
5794
|
+
while (end < columns && !labels[end]) end++;
|
|
5795
|
+
const element = span("heatmap-chart-column", text);
|
|
5796
|
+
element.classList.toggle("run", end > column + 1);
|
|
5797
|
+
element.style.gridArea = `${this.labels.length + 1} / ${column + 2} / auto / ${end + 2}`;
|
|
5798
|
+
grid.append(element);
|
|
5799
|
+
});
|
|
5800
|
+
return grid;
|
|
5801
|
+
}
|
|
5802
|
+
};
|
|
5803
|
+
|
|
5804
|
+
// src/ts/components/radarChart.ts
|
|
5805
|
+
var RINGS = 4;
|
|
5806
|
+
var RadarChart = class extends LineChart {
|
|
5807
|
+
get _type() {
|
|
5808
|
+
return "radar-chart";
|
|
5809
|
+
}
|
|
5810
|
+
get _key() {
|
|
5811
|
+
return "Expressive_RadarChart";
|
|
5812
|
+
}
|
|
5813
|
+
/** The scale starts at zero, the middle, so shapes compare by size. */
|
|
5814
|
+
get _fromZero() {
|
|
5815
|
+
return true;
|
|
5816
|
+
}
|
|
5817
|
+
get _circular() {
|
|
5818
|
+
return true;
|
|
5819
|
+
}
|
|
5820
|
+
static init(els, options = {}) {
|
|
5821
|
+
return super.init(els, options);
|
|
5822
|
+
}
|
|
5823
|
+
static getInstance(el) {
|
|
5824
|
+
return el["Expressive_RadarChart"];
|
|
5825
|
+
}
|
|
5826
|
+
/**
|
|
5827
|
+
* The cursor is a spoke turned by --turn, and the tooltip sits past its
|
|
5828
|
+
* tip. LineChart's points on the spoke keep their top, a share of the
|
|
5829
|
+
* radius, which stops at the rim and the middle like the shapes.
|
|
5830
|
+
*/
|
|
5831
|
+
_place(index) {
|
|
5832
|
+
for (const dot of this._cursor.children) {
|
|
5833
|
+
dot.style.top = `${Math.min(100, Math.max(0, parseFloat(dot.style.top)))}%`;
|
|
5834
|
+
}
|
|
5835
|
+
const turn = this._turn(index);
|
|
5836
|
+
this._cursor.style.setProperty("--turn", String(turn));
|
|
5837
|
+
this._tooltip.style.setProperty("--turn", String(turn));
|
|
5838
|
+
this._tooltip.classList.toggle("end", turn > 0 && turn < 0.5);
|
|
5839
|
+
}
|
|
5840
|
+
/** A row's spoke as a fraction of a turn clockwise from the top. */
|
|
5841
|
+
_turn(index) {
|
|
5842
|
+
return index / this.labels.length;
|
|
5843
|
+
}
|
|
5844
|
+
/** Only the legend; the labels go round the rim in _draw. */
|
|
5845
|
+
_guides() {
|
|
5846
|
+
return this._legend();
|
|
5847
|
+
}
|
|
5848
|
+
/** The nearest spoke to the pointer, by its angle from the middle. */
|
|
5849
|
+
_indexAt(e) {
|
|
5850
|
+
const [turn] = this._polarAt(e);
|
|
5851
|
+
return Math.round(turn * this.labels.length) % this.labels.length;
|
|
5852
|
+
}
|
|
5853
|
+
/**
|
|
5854
|
+
* An SVG in a -50 to 50 box: the rings and spokes of the grid, then each
|
|
5855
|
+
* series' shape, with its points for .points. The rim is the box's edge;
|
|
5856
|
+
* the labels round it are HTML.
|
|
5857
|
+
*/
|
|
5858
|
+
_draw() {
|
|
5859
|
+
const at = (index, share2) => {
|
|
5860
|
+
const angle = this._turn(index) * 2 * Math.PI;
|
|
5861
|
+
return `${(50 * share2 * Math.sin(angle)).toFixed(3)},${(-50 * share2 * Math.cos(angle)).toFixed(3)}`;
|
|
5862
|
+
};
|
|
5863
|
+
const share = (value) => Math.min(1, Math.max(0, 1 - this._y(value) / 100));
|
|
5864
|
+
const rows = this.labels.map((_, i) => i);
|
|
5865
|
+
const ring = (r) => `M${rows.map((i) => at(i, r)).join("L")}Z`;
|
|
5866
|
+
const svg = svgNode("svg", { viewBox: "-50 -50 100 100", "aria-hidden": "true", focusable: "false" });
|
|
5867
|
+
svg.append(svgNode("path", {
|
|
5868
|
+
class: "radar-chart-grid",
|
|
5869
|
+
d: Array.from({ length: RINGS }, (_, r) => ring((r + 1) / RINGS)).join("") + rows.map((i) => `M0,0L${at(i, 1)}`).join("")
|
|
5870
|
+
}));
|
|
5871
|
+
const points = this.el.classList.contains("points");
|
|
5872
|
+
this.series.forEach((series, s) => {
|
|
5873
|
+
const group = svgNode("g", {
|
|
5874
|
+
class: `radar-chart-series ${series.className}`.trim(),
|
|
5875
|
+
"data-series": String(s % COLORS + 1)
|
|
5876
|
+
});
|
|
5877
|
+
const drawn = rows.filter((i) => series.values[i] !== null);
|
|
5878
|
+
const corners = drawn.map((i) => at(i, share(series.values[i])));
|
|
5879
|
+
group.append(svgNode("path", { class: "radar-chart-shape", d: corners.length ? `M${corners.join("L")}Z` : "" }));
|
|
5880
|
+
if (points) group.append(svgNode("path", { class: "radar-chart-points", d: corners.map((c) => `M${c}h0`).join("") }));
|
|
5881
|
+
svg.append(group);
|
|
5882
|
+
});
|
|
5883
|
+
const labels = document.createElement("ol");
|
|
5884
|
+
labels.className = "radar-chart-axes";
|
|
5885
|
+
labels.setAttribute("aria-hidden", "true");
|
|
5886
|
+
this.labels.forEach((text, i) => {
|
|
5887
|
+
const li = document.createElement("li");
|
|
5888
|
+
li.textContent = text;
|
|
5889
|
+
li.style.setProperty("--turn", String(this._turn(i)));
|
|
5890
|
+
labels.append(li);
|
|
5891
|
+
});
|
|
5892
|
+
const web = document.createElement("div");
|
|
5893
|
+
web.className = "radar-chart-web";
|
|
5894
|
+
web.append(svg, labels);
|
|
5895
|
+
return web;
|
|
5896
|
+
}
|
|
5897
|
+
};
|
|
5898
|
+
|
|
5899
|
+
// src/ts/components/radialChart.ts
|
|
5900
|
+
var RADIUS2 = 46;
|
|
5901
|
+
var FILL = 0.8;
|
|
5902
|
+
var RadialChart = class extends LineChart {
|
|
5903
|
+
get _type() {
|
|
5904
|
+
return "radial-chart";
|
|
5905
|
+
}
|
|
5906
|
+
get _key() {
|
|
5907
|
+
return "Expressive_RadialChart";
|
|
5908
|
+
}
|
|
5909
|
+
get _points() {
|
|
5910
|
+
return false;
|
|
5911
|
+
}
|
|
5912
|
+
get _circular() {
|
|
5913
|
+
return true;
|
|
5914
|
+
}
|
|
5915
|
+
get _gauge() {
|
|
5916
|
+
return this.el.classList.contains("gauge");
|
|
3870
5917
|
}
|
|
3871
|
-
/**
|
|
3872
|
-
* Initializes instances of CharacterCounter.
|
|
3873
|
-
* @param els HTML elements.
|
|
3874
|
-
* @param options Component options.
|
|
3875
|
-
*/
|
|
3876
5918
|
static init(els, options = {}) {
|
|
3877
|
-
return super.init(els, options
|
|
5919
|
+
return super.init(els, options);
|
|
3878
5920
|
}
|
|
3879
5921
|
static getInstance(el) {
|
|
3880
|
-
return el["
|
|
5922
|
+
return el["Expressive_RadialChart"];
|
|
5923
|
+
}
|
|
5924
|
+
show(index) {
|
|
5925
|
+
if (!this._tooltip || index === this.activeIndex) return;
|
|
5926
|
+
super.show(index);
|
|
5927
|
+
this._rings.forEach((ring, i) => ring.classList.toggle("active", i === index));
|
|
5928
|
+
if (this._tooltip.hidden) return;
|
|
5929
|
+
const color = this._color(index);
|
|
5930
|
+
this._tooltip.querySelectorAll("[data-series]").forEach((span) => {
|
|
5931
|
+
span.dataset.series = color;
|
|
5932
|
+
});
|
|
3881
5933
|
}
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
this.
|
|
3885
|
-
this.
|
|
5934
|
+
/** The tooltip sits where the ring's fill ends. */
|
|
5935
|
+
_place(index) {
|
|
5936
|
+
const turn = this._turn(this._fractions[index]);
|
|
5937
|
+
const r = this._radii[index];
|
|
5938
|
+
const [x, y] = [r * Math.sin(turn * 2 * Math.PI), -r * Math.cos(turn * 2 * Math.PI)];
|
|
5939
|
+
this._tooltip.style.left = `${50 + x}%`;
|
|
5940
|
+
this._tooltip.style.top = `${(50 + y) / (this._gauge ? 50 : 100) * 100}%`;
|
|
5941
|
+
this._tooltip.classList.toggle("end", x > 0);
|
|
5942
|
+
}
|
|
5943
|
+
_color(index) {
|
|
5944
|
+
return String(index % COLORS + 1);
|
|
5945
|
+
}
|
|
5946
|
+
/** A fraction of the ring as a turn clockwise from the top. */
|
|
5947
|
+
_turn(fraction) {
|
|
5948
|
+
return this._gauge ? fraction / 2 - 0.25 : fraction;
|
|
5949
|
+
}
|
|
5950
|
+
/** For more than one row, a legend of every row with its color and cell. */
|
|
5951
|
+
_guides() {
|
|
5952
|
+
if (this.labels.length < 2) return [];
|
|
5953
|
+
const legend = document.createElement("ul");
|
|
5954
|
+
legend.className = "radial-chart-legend";
|
|
5955
|
+
legend.setAttribute("aria-hidden", "true");
|
|
5956
|
+
const text = this.series[0].text;
|
|
5957
|
+
legend.append(...this.labels.map((label, i) => {
|
|
5958
|
+
const li = document.createElement("li");
|
|
5959
|
+
li.dataset.series = this._color(i);
|
|
5960
|
+
const [name, value] = [label, text[i]].map((content) => {
|
|
5961
|
+
const span = document.createElement("span");
|
|
5962
|
+
span.textContent = content;
|
|
5963
|
+
return span;
|
|
5964
|
+
});
|
|
5965
|
+
li.append(name, value);
|
|
5966
|
+
return li;
|
|
5967
|
+
}));
|
|
5968
|
+
return [legend];
|
|
5969
|
+
}
|
|
5970
|
+
/** The ring under the pointer by its distance from the middle, or -1. */
|
|
5971
|
+
_indexAt(e) {
|
|
5972
|
+
const box = this._plot.getBoundingClientRect();
|
|
5973
|
+
const [x, y] = [e.clientX - box.left - box.width / 2, e.clientY - box.top - box.width / 2];
|
|
5974
|
+
if (this._gauge && y > 0) return -1;
|
|
5975
|
+
const distance = Math.hypot(x, y) / box.width * 100;
|
|
5976
|
+
const step = this._radii.length > 1 ? this._radii[0] - this._radii[1] : this._width;
|
|
5977
|
+
return this._radii.findIndex((r) => Math.abs(distance - r) <= step / 2);
|
|
3886
5978
|
}
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
5979
|
+
/**
|
|
5980
|
+
* An SVG of rings in a -50 to 50 box, the top half for a gauge. Each row
|
|
5981
|
+
* is a track circle and a fill circle whose dash is its share; CSS draws
|
|
5982
|
+
* them from --radial-chart-dash. The total is HTML over it.
|
|
5983
|
+
*/
|
|
5984
|
+
_draw() {
|
|
5985
|
+
const style = getComputedStyle(this.el);
|
|
5986
|
+
const token = (name, fallback) => {
|
|
5987
|
+
const value = parseFloat(style.getPropertyValue(`--md-comp-radial-chart-${name}`));
|
|
5988
|
+
return Number.isFinite(value) ? value : fallback;
|
|
5989
|
+
};
|
|
5990
|
+
const count = this.labels.length;
|
|
5991
|
+
let [width, gap] = [Math.max(0, token("thickness", 8)), Math.max(0, token("gap", 2))];
|
|
5992
|
+
const scale = Math.min(1, RADIUS2 * FILL / (count * width + (count - 1) * gap || 1));
|
|
5993
|
+
[width, gap] = [width * scale, gap * scale];
|
|
5994
|
+
this._width = width;
|
|
5995
|
+
this._radii = this.labels.map((_, i) => RADIUS2 - width / 2 - i * (width + gap));
|
|
5996
|
+
const max = [parseFloat(this.el.dataset.max ?? ""), this.options.max ?? NaN, 100].find((value) => value > 0);
|
|
5997
|
+
this._fractions = this.series[0].values.map((value) => Math.min(1, Math.max(0, (value ?? 0) / max)));
|
|
5998
|
+
const gauge = this._gauge;
|
|
5999
|
+
const svg = svgNode("svg", { viewBox: gauge ? "-50 -50 100 50" : "-50 -50 100 100", focusable: "false" });
|
|
6000
|
+
const transform = gauge ? "rotate(180)" : "rotate(-90)";
|
|
6001
|
+
const span = gauge ? 0.5 : 1;
|
|
6002
|
+
this._rings = this._radii.map((r, i) => {
|
|
6003
|
+
const length = 2 * Math.PI * r;
|
|
6004
|
+
const circle = (name, fraction, capped) => {
|
|
6005
|
+
const ring2 = svgNode("circle", { class: name, r: r.toFixed(3), transform, "stroke-width": width.toFixed(3) });
|
|
6006
|
+
const dash = Math.max(1e-3, fraction * span * length - (capped ? width : 0));
|
|
6007
|
+
ring2.style.setProperty("--radial-chart-dash", `${dash.toFixed(3)} ${length.toFixed(3)}`);
|
|
6008
|
+
if (capped) ring2.style.setProperty("--radial-chart-offset", (-width / 2).toFixed(3));
|
|
6009
|
+
return ring2;
|
|
6010
|
+
};
|
|
6011
|
+
const ring = circle("radial-chart-ring", this._fractions[i], true);
|
|
6012
|
+
ring.dataset.series = this._color(i);
|
|
6013
|
+
if (!this._fractions[i]) ring.classList.add("empty");
|
|
6014
|
+
svg.append(circle("radial-chart-track", 1, gauge), ring);
|
|
6015
|
+
return ring;
|
|
6016
|
+
});
|
|
6017
|
+
const rings = document.createElement("div");
|
|
6018
|
+
rings.className = "radial-chart-rings";
|
|
6019
|
+
rings.setAttribute("aria-hidden", "true");
|
|
6020
|
+
rings.style.setProperty("--radial-chart-middle", `${(this._radii[count - 1] - this._width / 2) * 2}%`);
|
|
6021
|
+
rings.append(svg);
|
|
6022
|
+
const total = this._total("radial-chart-total");
|
|
6023
|
+
if (total) rings.append(total);
|
|
6024
|
+
return rings;
|
|
3890
6025
|
}
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
6026
|
+
};
|
|
6027
|
+
|
|
6028
|
+
// src/ts/components/mixedChart.ts
|
|
6029
|
+
var has = (series, name) => series.className.split(/\s+/).includes(name);
|
|
6030
|
+
var MixedChart = class extends ColumnChart {
|
|
6031
|
+
get _type() {
|
|
6032
|
+
return "mixed-chart";
|
|
3894
6033
|
}
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
this.counterEl.classList.add("character-counter");
|
|
3898
|
-
this.counterEl.setAttribute("aria-live", "polite");
|
|
3899
|
-
this.counterEl.setAttribute("aria-atomic", "true");
|
|
3900
|
-
this.counterEl.style.float = "right";
|
|
3901
|
-
this.counterEl.style.fontSize = "12px";
|
|
3902
|
-
this.counterEl.style.height = "1";
|
|
3903
|
-
this.el.parentElement.appendChild(this.counterEl);
|
|
6034
|
+
get _key() {
|
|
6035
|
+
return "Expressive_MixedChart";
|
|
3904
6036
|
}
|
|
3905
|
-
|
|
3906
|
-
|
|
6037
|
+
get _points() {
|
|
6038
|
+
return true;
|
|
3907
6039
|
}
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
6040
|
+
static init(els, options = {}) {
|
|
6041
|
+
return super.init(els, options);
|
|
6042
|
+
}
|
|
6043
|
+
static getInstance(el) {
|
|
6044
|
+
return el["Expressive_MixedChart"];
|
|
6045
|
+
}
|
|
6046
|
+
/** The start scale, then the end scale; each includes zero if it has columns. */
|
|
6047
|
+
_scale(series, min, max) {
|
|
6048
|
+
const end = series.filter((each) => has(each, "end"));
|
|
6049
|
+
const start = series.filter((each) => !end.includes(each));
|
|
6050
|
+
const columns = (list) => list.some((each) => has(each, "column"));
|
|
6051
|
+
super._scale(end, this._number("endMin"), this._number("endMax"), columns(end));
|
|
6052
|
+
return super._scale(start, min, max, columns(start));
|
|
6053
|
+
}
|
|
6054
|
+
/** Columns behind, then the lines and areas over them. */
|
|
6055
|
+
_draw() {
|
|
6056
|
+
const marks = document.createElement("div");
|
|
6057
|
+
marks.className = "mixed-chart-marks";
|
|
6058
|
+
const columns = super._draw(this.series.filter((series) => has(series, "column")));
|
|
6059
|
+
columns.className = "mixed-chart-columns";
|
|
6060
|
+
marks.append(columns, LineChart.prototype["_draw"].call(this, this.series.filter((series) => !has(series, "column"))));
|
|
6061
|
+
return marks;
|
|
3917
6062
|
}
|
|
3918
6063
|
};
|
|
3919
6064
|
|
|
3920
6065
|
// src/ts/components/select.ts
|
|
3921
|
-
var
|
|
6066
|
+
var _defaults17 = {
|
|
3922
6067
|
classes: "",
|
|
3923
6068
|
menuOptions: {}
|
|
3924
6069
|
};
|
|
@@ -4000,7 +6145,7 @@ var FormSelect = class _FormSelect extends Component {
|
|
|
4000
6145
|
this._setupEventHandlers();
|
|
4001
6146
|
}
|
|
4002
6147
|
static get defaults() {
|
|
4003
|
-
return
|
|
6148
|
+
return _defaults17;
|
|
4004
6149
|
}
|
|
4005
6150
|
/**
|
|
4006
6151
|
* Initializes instances of FormSelect.
|
|
@@ -4425,7 +6570,7 @@ var FormSelect = class _FormSelect extends Component {
|
|
|
4425
6570
|
};
|
|
4426
6571
|
|
|
4427
6572
|
// src/ts/plugins/dockedDisplayPlugin.ts
|
|
4428
|
-
var
|
|
6573
|
+
var _defaults18 = {
|
|
4429
6574
|
margin: 5,
|
|
4430
6575
|
transition: 10,
|
|
4431
6576
|
duration: 250,
|
|
@@ -4439,9 +6584,9 @@ var DockedDisplayPlugin = class _DockedDisplayPlugin {
|
|
|
4439
6584
|
}
|
|
4440
6585
|
};
|
|
4441
6586
|
/**
|
|
4442
|
-
* Tears the plugin down.
|
|
4443
|
-
* instance - and through it the whole picker subtree - so
|
|
4444
|
-
* component must call this from its own destroy().
|
|
6587
|
+
* Tears the plugin down. While shown, the document listener holds a
|
|
6588
|
+
* reference to this instance - and through it the whole picker subtree - so
|
|
6589
|
+
* the owning component must call this from its own destroy().
|
|
4445
6590
|
*/
|
|
4446
6591
|
this.destroy = () => {
|
|
4447
6592
|
document.removeEventListener("click", this._handleDocumentClick);
|
|
@@ -4450,6 +6595,7 @@ var DockedDisplayPlugin = class _DockedDisplayPlugin {
|
|
|
4450
6595
|
this.show = () => {
|
|
4451
6596
|
if (this.visible) return;
|
|
4452
6597
|
this.visible = true;
|
|
6598
|
+
document.addEventListener("click", this._handleDocumentClick);
|
|
4453
6599
|
const coordinates = Utils._setAbsolutePosition(this.el, this.container, "bottom", this.options.margin, this.options.transition, this.options.align);
|
|
4454
6600
|
this.container.style.visibility = "visible";
|
|
4455
6601
|
this.container.style.transition = `
|
|
@@ -4463,6 +6609,7 @@ var DockedDisplayPlugin = class _DockedDisplayPlugin {
|
|
|
4463
6609
|
this.hide = () => {
|
|
4464
6610
|
if (!this.visible) return;
|
|
4465
6611
|
this.visible = false;
|
|
6612
|
+
document.removeEventListener("click", this._handleDocumentClick);
|
|
4466
6613
|
this.container.removeAttribute("style");
|
|
4467
6614
|
this.container.style.transition = `
|
|
4468
6615
|
transform ${this.options.duration}ms ease-out,
|
|
@@ -4474,14 +6621,13 @@ var DockedDisplayPlugin = class _DockedDisplayPlugin {
|
|
|
4474
6621
|
};
|
|
4475
6622
|
this.el = el;
|
|
4476
6623
|
this.options = {
|
|
4477
|
-
...
|
|
6624
|
+
..._defaults18,
|
|
4478
6625
|
...options
|
|
4479
6626
|
};
|
|
4480
6627
|
this.container = document.createElement("div");
|
|
4481
6628
|
this.container.classList.add("display-docked");
|
|
4482
6629
|
this.container.append(container);
|
|
4483
6630
|
el.parentElement.append(this.container);
|
|
4484
|
-
document.addEventListener("click", this._handleDocumentClick);
|
|
4485
6631
|
}
|
|
4486
6632
|
/**
|
|
4487
6633
|
* Initializes instance of DockedDisplayPlugin
|
|
@@ -4495,7 +6641,7 @@ var DockedDisplayPlugin = class _DockedDisplayPlugin {
|
|
|
4495
6641
|
};
|
|
4496
6642
|
|
|
4497
6643
|
// src/ts/components/datepicker.ts
|
|
4498
|
-
var
|
|
6644
|
+
var _defaults19 = {
|
|
4499
6645
|
// the default output format for the input field value
|
|
4500
6646
|
format: "mmm dd, yyyy",
|
|
4501
6647
|
// Used to create date object from current input string
|
|
@@ -4599,6 +6745,8 @@ var _Datepicker = class _Datepicker extends Component {
|
|
|
4599
6745
|
constructor(el, options) {
|
|
4600
6746
|
super(el, options, _Datepicker);
|
|
4601
6747
|
this.multiple = false;
|
|
6748
|
+
/** Rows renderRow has already checked, so renderBody does not parse them again. */
|
|
6749
|
+
this._checkedRows = /* @__PURE__ */ new Set();
|
|
4602
6750
|
/** Nesting depth of {@link _batchDraws}; > 0 means draws are being collected. */
|
|
4603
6751
|
this._drawDepth = 0;
|
|
4604
6752
|
/** Whether a draw was requested while batching. */
|
|
@@ -4665,7 +6813,9 @@ var _Datepicker = class _Datepicker extends Component {
|
|
|
4665
6813
|
if (maxDate && date > maxDate) date.setTime(maxDate.getTime());
|
|
4666
6814
|
if (date.getFullYear() < minYear || date.getFullYear() > maxYear || date.getFullYear() === minYear && date.getMonth() < (minMonth ?? 0) || date.getFullYear() === maxYear && date.getMonth() > (maxMonth ?? 11)) return;
|
|
4667
6815
|
this._focusedDate = date;
|
|
4668
|
-
this.
|
|
6816
|
+
const { year, month } = this.calendars[0];
|
|
6817
|
+
if (date.getFullYear() === year && date.getMonth() === month) this._syncCalendarFocus(true);
|
|
6818
|
+
else this.gotoDate(date);
|
|
4669
6819
|
};
|
|
4670
6820
|
this._handleCalendarClick = (e) => {
|
|
4671
6821
|
const target = e.target;
|
|
@@ -4780,7 +6930,7 @@ var _Datepicker = class _Datepicker extends Component {
|
|
|
4780
6930
|
}
|
|
4781
6931
|
}
|
|
4782
6932
|
static get defaults() {
|
|
4783
|
-
return
|
|
6933
|
+
return _defaults19;
|
|
4784
6934
|
}
|
|
4785
6935
|
/**
|
|
4786
6936
|
* Initializes instances of Datepicker.
|
|
@@ -5222,7 +7372,9 @@ var _Datepicker = class _Datepicker extends Component {
|
|
|
5222
7372
|
}
|
|
5223
7373
|
/** Assemble calendar cells; reject unsupported elements and attributes. */
|
|
5224
7374
|
renderRow(days, isRTL, isRowSelected) {
|
|
5225
|
-
|
|
7375
|
+
const row = '<tr class="datepicker-row' + (isRowSelected ? " is-selected" : "") + '">' + _Datepicker._calendarMarkup((isRTL ? days.reverse() : days).join(""), "tr") + "</tr>";
|
|
7376
|
+
this._checkedRows.add(row);
|
|
7377
|
+
return row;
|
|
5226
7378
|
}
|
|
5227
7379
|
/** Assemble calendar rows; reject unsupported elements and attributes. */
|
|
5228
7380
|
renderTable(opts, data, randId) {
|
|
@@ -5240,7 +7392,10 @@ var _Datepicker = class _Datepicker extends Component {
|
|
|
5240
7392
|
}
|
|
5241
7393
|
/** Wrap calendar rows after validating and encoding their contents. */
|
|
5242
7394
|
renderBody(rows) {
|
|
5243
|
-
|
|
7395
|
+
const html = rows.join("");
|
|
7396
|
+
const checked = rows.every((row) => this._checkedRows.has(row));
|
|
7397
|
+
this._checkedRows.clear();
|
|
7398
|
+
return "<tbody>" + (checked ? html : _Datepicker._calendarMarkup(html, "tbody")) + "</tbody>";
|
|
5244
7399
|
}
|
|
5245
7400
|
static _calendarMarkup(html, parent) {
|
|
5246
7401
|
const template = document.createElement("template");
|
|
@@ -5595,8 +7750,789 @@ _Datepicker._template = `
|
|
|
5595
7750
|
</div>`;
|
|
5596
7751
|
var Datepicker = _Datepicker;
|
|
5597
7752
|
|
|
7753
|
+
// src/ts/components/messageScroller.ts
|
|
7754
|
+
var _defaults20 = {
|
|
7755
|
+
autoScroll: true,
|
|
7756
|
+
defaultScrollPosition: "end",
|
|
7757
|
+
peek: 64
|
|
7758
|
+
};
|
|
7759
|
+
var EDGE = 2;
|
|
7760
|
+
var UP_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "PageUp", "Home"]);
|
|
7761
|
+
var RELEASE_EVENTS = ["pointerup", "pointercancel", "touchend", "touchcancel"];
|
|
7762
|
+
var MessageScroller = class _MessageScroller extends Component {
|
|
7763
|
+
constructor(el, options) {
|
|
7764
|
+
super(el, options, _MessageScroller);
|
|
7765
|
+
this._opened = false;
|
|
7766
|
+
this._following = false;
|
|
7767
|
+
/** A press is down: it may be a tap or a scrollbar drag. */
|
|
7768
|
+
this._holding = false;
|
|
7769
|
+
this._lastTop = 0;
|
|
7770
|
+
/** Where a programmatic scroll is heading; null when none is running. */
|
|
7771
|
+
this._target = null;
|
|
7772
|
+
/** Distance left to the target at the last scroll event. */
|
|
7773
|
+
this._distance = Infinity;
|
|
7774
|
+
/** The first visible message and its offset from the viewport top. */
|
|
7775
|
+
this._ref = null;
|
|
7776
|
+
this._refOffset = 0;
|
|
7777
|
+
/** The latest anchored turn, which the spacer lets reach the top. */
|
|
7778
|
+
this._anchor = null;
|
|
7779
|
+
this._spacer = 0;
|
|
7780
|
+
this._last = null;
|
|
7781
|
+
this._resizeObserver = null;
|
|
7782
|
+
this._mutationObserver = null;
|
|
7783
|
+
// A programmatic scroll ends when it arrives, or when a scroll moves away
|
|
7784
|
+
// from its target because something else took over.
|
|
7785
|
+
this._handleScroll = () => {
|
|
7786
|
+
if (this._target !== null) {
|
|
7787
|
+
const distance = Math.abs(this.viewport.scrollTop - this._resolve(this._target));
|
|
7788
|
+
if (distance > EDGE && distance < this._distance) {
|
|
7789
|
+
this._distance = distance;
|
|
7790
|
+
return;
|
|
7791
|
+
}
|
|
7792
|
+
this._target = null;
|
|
7793
|
+
}
|
|
7794
|
+
this._settle();
|
|
7795
|
+
};
|
|
7796
|
+
this._handleUserScroll = (e) => {
|
|
7797
|
+
if (e.type === "wheel" && e.deltaY >= 0) return;
|
|
7798
|
+
if (e.type === "keydown" && !UP_KEYS.has(e.key)) return;
|
|
7799
|
+
this._target = null;
|
|
7800
|
+
if (e.type === "pointerdown" || e.type === "touchstart") this._holding = true;
|
|
7801
|
+
else this._following = false;
|
|
7802
|
+
};
|
|
7803
|
+
this._handleRelease = () => {
|
|
7804
|
+
if (!this._holding) return;
|
|
7805
|
+
this._holding = false;
|
|
7806
|
+
if (this._following) this.viewport.scrollTop = this.viewport.scrollHeight;
|
|
7807
|
+
this._sync();
|
|
7808
|
+
};
|
|
7809
|
+
this._handleButtonClick = () => this.scrollToEnd();
|
|
7810
|
+
this._handleMutations = (records) => {
|
|
7811
|
+
for (const record of records) {
|
|
7812
|
+
record.addedNodes.forEach((node) => {
|
|
7813
|
+
if (node instanceof Element) this._resizeObserver?.observe(node);
|
|
7814
|
+
});
|
|
7815
|
+
record.removedNodes.forEach((node) => {
|
|
7816
|
+
if (node instanceof Element) this._resizeObserver?.unobserve(node);
|
|
7817
|
+
});
|
|
7818
|
+
}
|
|
7819
|
+
const previous = this._last;
|
|
7820
|
+
this._last = this.viewport.lastElementChild;
|
|
7821
|
+
if (!this._opened) return;
|
|
7822
|
+
if (!this._following && this._target === null) this._restore();
|
|
7823
|
+
if (!this.options.autoScroll) return;
|
|
7824
|
+
if (previous && previous.parentElement !== this.viewport) return;
|
|
7825
|
+
for (let item = this._last; item && item !== previous; item = item.previousElementSibling) {
|
|
7826
|
+
if (item.hasAttribute("data-scroll-anchor")) {
|
|
7827
|
+
this._anchor = item;
|
|
7828
|
+
this._updateSpacer();
|
|
7829
|
+
this._following = true;
|
|
7830
|
+
this._scrollTo(this._top(item) - this.options.peek);
|
|
7831
|
+
return;
|
|
7832
|
+
}
|
|
7833
|
+
}
|
|
7834
|
+
};
|
|
7835
|
+
this._handleResize = () => {
|
|
7836
|
+
if (!this._opened) {
|
|
7837
|
+
if (this.viewport.clientHeight > 0) this._open();
|
|
7838
|
+
return;
|
|
7839
|
+
}
|
|
7840
|
+
this._updateSpacer();
|
|
7841
|
+
const v = this.viewport;
|
|
7842
|
+
if (this._target === "end") this._scrollTo("end");
|
|
7843
|
+
else if (this._target === null) {
|
|
7844
|
+
if (this._following && !this._holding) v.scrollTop = v.scrollHeight;
|
|
7845
|
+
else this._restore();
|
|
7846
|
+
}
|
|
7847
|
+
this._sync();
|
|
7848
|
+
};
|
|
7849
|
+
this.el["Expressive_MessageScroller"] = this;
|
|
7850
|
+
this.options = {
|
|
7851
|
+
..._MessageScroller.defaults,
|
|
7852
|
+
...options
|
|
7853
|
+
};
|
|
7854
|
+
this.viewport = el.querySelector(":scope > .message-scroller-viewport");
|
|
7855
|
+
this.button = el.querySelector(":scope > .message-scroller-button");
|
|
7856
|
+
if (!this.viewport) {
|
|
7857
|
+
console.error("MessageScroller needs a .message-scroller-viewport child.");
|
|
7858
|
+
return;
|
|
7859
|
+
}
|
|
7860
|
+
this._last = this.viewport.lastElementChild;
|
|
7861
|
+
if (this.viewport.clientHeight > 0) this._open();
|
|
7862
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
7863
|
+
this._resizeObserver = new ResizeObserver(this._handleResize);
|
|
7864
|
+
this._resizeObserver.observe(this.viewport, { box: "border-box" });
|
|
7865
|
+
for (const item of Array.from(this.viewport.children)) this._resizeObserver.observe(item);
|
|
7866
|
+
}
|
|
7867
|
+
this._mutationObserver = new MutationObserver(this._handleMutations);
|
|
7868
|
+
this._mutationObserver.observe(this.viewport, { childList: true });
|
|
7869
|
+
this.viewport.addEventListener("scroll", this._handleScroll, { passive: true });
|
|
7870
|
+
for (const type of ["wheel", "touchstart", "pointerdown", "keydown"]) {
|
|
7871
|
+
this.viewport.addEventListener(type, this._handleUserScroll, { passive: true });
|
|
7872
|
+
}
|
|
7873
|
+
for (const type of RELEASE_EVENTS) document.addEventListener(type, this._handleRelease);
|
|
7874
|
+
this.button?.addEventListener("click", this._handleButtonClick);
|
|
7875
|
+
}
|
|
7876
|
+
static get defaults() {
|
|
7877
|
+
return _defaults20;
|
|
7878
|
+
}
|
|
7879
|
+
/**
|
|
7880
|
+
* Initializes instances of MessageScroller.
|
|
7881
|
+
* @param els HTML elements.
|
|
7882
|
+
* @param options Component options.
|
|
7883
|
+
*/
|
|
7884
|
+
static init(els, options = {}) {
|
|
7885
|
+
return super.init(els, options, _MessageScroller);
|
|
7886
|
+
}
|
|
7887
|
+
static getInstance(el) {
|
|
7888
|
+
return el["Expressive_MessageScroller"];
|
|
7889
|
+
}
|
|
7890
|
+
destroy() {
|
|
7891
|
+
this.el["Expressive_MessageScroller"] = void 0;
|
|
7892
|
+
if (!this.viewport) return;
|
|
7893
|
+
this._resizeObserver?.disconnect();
|
|
7894
|
+
this._mutationObserver?.disconnect();
|
|
7895
|
+
this.viewport.removeEventListener("scroll", this._handleScroll);
|
|
7896
|
+
for (const type of ["wheel", "touchstart", "pointerdown", "keydown"]) {
|
|
7897
|
+
this.viewport.removeEventListener(type, this._handleUserScroll);
|
|
7898
|
+
}
|
|
7899
|
+
for (const type of RELEASE_EVENTS) document.removeEventListener(type, this._handleRelease);
|
|
7900
|
+
this.viewport.style.removeProperty("--md-comp-message-scroller-spacer");
|
|
7901
|
+
if (this.button) {
|
|
7902
|
+
this.button.removeEventListener("click", this._handleButtonClick);
|
|
7903
|
+
this.button.inert = false;
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7906
|
+
/** Scrolls to the latest message and resumes following. */
|
|
7907
|
+
scrollToEnd() {
|
|
7908
|
+
this._following = this.options.autoScroll;
|
|
7909
|
+
this._scrollTo("end");
|
|
7910
|
+
}
|
|
7911
|
+
/** Scrolls to the first message. */
|
|
7912
|
+
scrollToStart() {
|
|
7913
|
+
this._following = false;
|
|
7914
|
+
this._scrollTo(0);
|
|
7915
|
+
}
|
|
7916
|
+
/**
|
|
7917
|
+
* Scrolls the element with this id to the top of the viewport, below the
|
|
7918
|
+
* peek, and stops following.
|
|
7919
|
+
* @returns false when no such element is inside the viewport.
|
|
7920
|
+
*/
|
|
7921
|
+
scrollToMessage(id) {
|
|
7922
|
+
const target = Utils.getElementById(this.viewport, id);
|
|
7923
|
+
if (!target || !this.viewport.contains(target)) return false;
|
|
7924
|
+
this._following = false;
|
|
7925
|
+
this._scrollTo(this._top(target) - this.options.peek);
|
|
7926
|
+
return true;
|
|
7927
|
+
}
|
|
7928
|
+
_open() {
|
|
7929
|
+
this._opened = true;
|
|
7930
|
+
const v = this.viewport;
|
|
7931
|
+
const position = this.options.defaultScrollPosition;
|
|
7932
|
+
const anchors = v.querySelectorAll(":scope > [data-scroll-anchor]");
|
|
7933
|
+
const anchor = position === "last-anchor" ? anchors[anchors.length - 1] : null;
|
|
7934
|
+
if (position === "start") v.scrollTop = 0;
|
|
7935
|
+
else if (anchor && this._contentEnd() - this._top(anchor) + this.options.peek > v.clientHeight) {
|
|
7936
|
+
v.scrollTop = this._top(anchor) - this.options.peek;
|
|
7937
|
+
} else v.scrollTop = v.scrollHeight;
|
|
7938
|
+
this._settle();
|
|
7939
|
+
}
|
|
7940
|
+
/**
|
|
7941
|
+
* Records where the reader is once no programmatic scroll is running.
|
|
7942
|
+
* Content can grow between a scroll and the resize that re-pins the end,
|
|
7943
|
+
* so only moving toward the start releases following.
|
|
7944
|
+
*/
|
|
7945
|
+
_settle() {
|
|
7946
|
+
const top = this.viewport.scrollTop;
|
|
7947
|
+
const kept = this._following && top >= this._lastTop - EDGE;
|
|
7948
|
+
this._following = this.options.autoScroll && (kept || this._atEnd());
|
|
7949
|
+
this._lastTop = top;
|
|
7950
|
+
this._capture();
|
|
7951
|
+
this._sync();
|
|
7952
|
+
}
|
|
7953
|
+
_scrollTo(target) {
|
|
7954
|
+
const v = this.viewport;
|
|
7955
|
+
if (Math.abs(v.scrollTop - this._resolve(target)) <= EDGE) {
|
|
7956
|
+
this._target = null;
|
|
7957
|
+
this._settle();
|
|
7958
|
+
return;
|
|
7959
|
+
}
|
|
7960
|
+
this._target = target;
|
|
7961
|
+
this._distance = Infinity;
|
|
7962
|
+
const reduce = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
7963
|
+
v.scrollTo({
|
|
7964
|
+
top: target === "end" ? v.scrollHeight : target,
|
|
7965
|
+
behavior: reduce ? "auto" : "smooth"
|
|
7966
|
+
});
|
|
7967
|
+
}
|
|
7968
|
+
_resolve(target) {
|
|
7969
|
+
const max = this.viewport.scrollHeight - this.viewport.clientHeight;
|
|
7970
|
+
return target === "end" ? max : Math.max(0, Math.min(target, max));
|
|
7971
|
+
}
|
|
7972
|
+
_atEnd() {
|
|
7973
|
+
const v = this.viewport;
|
|
7974
|
+
return v.scrollHeight - v.scrollTop - v.clientHeight <= EDGE;
|
|
7975
|
+
}
|
|
7976
|
+
/** Offset of an element from the top of the scrolled content. */
|
|
7977
|
+
_top(el) {
|
|
7978
|
+
const v = this.viewport;
|
|
7979
|
+
return el.getBoundingClientRect().top - v.getBoundingClientRect().top - v.clientTop + v.scrollTop;
|
|
7980
|
+
}
|
|
7981
|
+
/** Bottom of the content without the spacer, padding included. */
|
|
7982
|
+
_contentEnd() {
|
|
7983
|
+
const last = this.viewport.lastElementChild;
|
|
7984
|
+
if (!last) return 0;
|
|
7985
|
+
const padding = parseFloat(getComputedStyle(this.viewport).paddingBlockEnd) - this._spacer;
|
|
7986
|
+
return this._top(last) + last.getBoundingClientRect().height + padding;
|
|
7987
|
+
}
|
|
7988
|
+
/** Remembers the first visible message so prepends can keep it in place. */
|
|
7989
|
+
_capture() {
|
|
7990
|
+
const items = this.viewport.children;
|
|
7991
|
+
const top = this.viewport.getBoundingClientRect().top + this.viewport.clientTop;
|
|
7992
|
+
let low = 0;
|
|
7993
|
+
let high = items.length - 1;
|
|
7994
|
+
while (low < high) {
|
|
7995
|
+
const mid = low + high >> 1;
|
|
7996
|
+
if (items[mid].getBoundingClientRect().bottom > top) high = mid;
|
|
7997
|
+
else low = mid + 1;
|
|
7998
|
+
}
|
|
7999
|
+
this._ref = items[low] ?? null;
|
|
8000
|
+
this._refOffset = this._ref ? this._ref.getBoundingClientRect().top - top : 0;
|
|
8001
|
+
}
|
|
8002
|
+
/** Moves the remembered message back to where the reader last saw it. */
|
|
8003
|
+
_restore() {
|
|
8004
|
+
if (this._ref?.parentElement !== this.viewport) {
|
|
8005
|
+
this._capture();
|
|
8006
|
+
return;
|
|
8007
|
+
}
|
|
8008
|
+
const top = this.viewport.getBoundingClientRect().top + this.viewport.clientTop;
|
|
8009
|
+
const delta = this._ref.getBoundingClientRect().top - top - this._refOffset;
|
|
8010
|
+
if (Math.abs(delta) >= 1) this.viewport.scrollTop += delta;
|
|
8011
|
+
}
|
|
8012
|
+
/** Sizes the blank space that lets the latest anchored turn reach the top. */
|
|
8013
|
+
_updateSpacer() {
|
|
8014
|
+
let space = 0;
|
|
8015
|
+
if (this._anchor?.parentElement === this.viewport) {
|
|
8016
|
+
const room = this._top(this._anchor) - this.options.peek + this.viewport.clientHeight;
|
|
8017
|
+
space = Math.max(0, Math.ceil(room - this._contentEnd()));
|
|
8018
|
+
} else this._anchor = null;
|
|
8019
|
+
if (space === this._spacer) return;
|
|
8020
|
+
this._spacer = space;
|
|
8021
|
+
if (space) this.viewport.style.setProperty("--md-comp-message-scroller-spacer", `${space}px`);
|
|
8022
|
+
else this.viewport.style.removeProperty("--md-comp-message-scroller-spacer");
|
|
8023
|
+
}
|
|
8024
|
+
/** The button is inert while there is nothing below. */
|
|
8025
|
+
_sync() {
|
|
8026
|
+
if (!this.button) return;
|
|
8027
|
+
const idle = this._atEnd();
|
|
8028
|
+
const root = this.button.getRootNode();
|
|
8029
|
+
if (idle && root.activeElement === this.button) this.viewport.focus({ preventScroll: true });
|
|
8030
|
+
this.button.inert = idle;
|
|
8031
|
+
}
|
|
8032
|
+
};
|
|
8033
|
+
|
|
8034
|
+
// src/ts/components/richTextEditor.ts
|
|
8035
|
+
var _defaults21 = {
|
|
8036
|
+
Editor: null,
|
|
8037
|
+
extensions: []
|
|
8038
|
+
};
|
|
8039
|
+
var NAMING = ["aria-label", "aria-labelledby", "aria-describedby"];
|
|
8040
|
+
var RichTextEditor = class _RichTextEditor extends Component {
|
|
8041
|
+
constructor(el, options) {
|
|
8042
|
+
super(el, options, _RichTextEditor);
|
|
8043
|
+
/** The Tiptap editor. */
|
|
8044
|
+
this.editor = null;
|
|
8045
|
+
/** The element the editor is mounted in. */
|
|
8046
|
+
this.content = null;
|
|
8047
|
+
this._actions = /* @__PURE__ */ new Map();
|
|
8048
|
+
this._label = null;
|
|
8049
|
+
this._handleClick = (e) => {
|
|
8050
|
+
const button = e.target.closest("button[data-command]");
|
|
8051
|
+
const action = this._actions.get(button);
|
|
8052
|
+
if (action && !button.disabled) this.editor.chain().focus()[action.command](action.attributes).run();
|
|
8053
|
+
};
|
|
8054
|
+
this._handleLabelClick = () => {
|
|
8055
|
+
this.editor.chain().focus().run();
|
|
8056
|
+
};
|
|
8057
|
+
this._handleUpdate = () => {
|
|
8058
|
+
this.textarea.value = this.editor.isEmpty ? "" : this.editor.getHTML();
|
|
8059
|
+
this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
8060
|
+
};
|
|
8061
|
+
this._handleTransaction = () => {
|
|
8062
|
+
const can = this.editor.isEditable && this.editor.can();
|
|
8063
|
+
for (const [button, { command, attributes, active }] of this._actions) {
|
|
8064
|
+
button.disabled = !can || !(command in this.editor.commands) || !can[command](attributes);
|
|
8065
|
+
if (!active) continue;
|
|
8066
|
+
const on = this.editor.isActive(active, attributes);
|
|
8067
|
+
button.setAttribute("aria-pressed", String(on));
|
|
8068
|
+
button.classList.toggle("active", on);
|
|
8069
|
+
}
|
|
8070
|
+
};
|
|
8071
|
+
this.el["Expressive_RichTextEditor"] = this;
|
|
8072
|
+
this.options = {
|
|
8073
|
+
..._RichTextEditor.defaults,
|
|
8074
|
+
...options
|
|
8075
|
+
};
|
|
8076
|
+
this.textarea = el.querySelector(":scope > textarea");
|
|
8077
|
+
if (!this.textarea || !this.options.Editor) {
|
|
8078
|
+
console.error("RichTextEditor needs a <textarea> child and the Tiptap Editor class in options.Editor.");
|
|
8079
|
+
return;
|
|
8080
|
+
}
|
|
8081
|
+
for (const button of el.querySelectorAll("button[data-command]")) {
|
|
8082
|
+
const command = button.dataset.command;
|
|
8083
|
+
const attributes2 = button.dataset.attributes ? JSON.parse(button.dataset.attributes) : void 0;
|
|
8084
|
+
const active = command.startsWith("toggle") ? command[6].toLowerCase() + command.slice(7) : void 0;
|
|
8085
|
+
this._actions.set(button, { command, attributes: attributes2, active });
|
|
8086
|
+
}
|
|
8087
|
+
const attributes = { role: "textbox", "aria-multiline": "true" };
|
|
8088
|
+
for (const name of NAMING) {
|
|
8089
|
+
const value = this.textarea.getAttribute(name);
|
|
8090
|
+
if (value) attributes[name] = value;
|
|
8091
|
+
}
|
|
8092
|
+
this._label = this.textarea.labels?.[0] ?? null;
|
|
8093
|
+
if (this._label && !attributes["aria-labelledby"]) {
|
|
8094
|
+
if (!this._label.id) this._label.id = `rich-text-editor-label-${Utils.guid()}`;
|
|
8095
|
+
attributes["aria-labelledby"] = this._label.id;
|
|
8096
|
+
this._label.addEventListener("click", this._handleLabelClick);
|
|
8097
|
+
}
|
|
8098
|
+
this.content = document.createElement("div");
|
|
8099
|
+
this.content.className = "rich-text-editor-content";
|
|
8100
|
+
this.textarea.after(this.content);
|
|
8101
|
+
this.textarea.hidden = true;
|
|
8102
|
+
this.editor = new this.options.Editor({
|
|
8103
|
+
element: this.content,
|
|
8104
|
+
extensions: this.options.extensions,
|
|
8105
|
+
content: this.textarea.value,
|
|
8106
|
+
editable: !(this.textarea.disabled || this.textarea.readOnly),
|
|
8107
|
+
editorProps: { attributes }
|
|
8108
|
+
});
|
|
8109
|
+
this.editor.on("update", this._handleUpdate);
|
|
8110
|
+
this.editor.on("transaction", this._handleTransaction);
|
|
8111
|
+
this._handleTransaction();
|
|
8112
|
+
el.addEventListener("click", this._handleClick);
|
|
8113
|
+
}
|
|
8114
|
+
static get defaults() {
|
|
8115
|
+
return _defaults21;
|
|
8116
|
+
}
|
|
8117
|
+
/**
|
|
8118
|
+
* Initializes instances of RichTextEditor.
|
|
8119
|
+
* @param els HTML elements.
|
|
8120
|
+
* @param options Component options.
|
|
8121
|
+
*/
|
|
8122
|
+
static init(els, options = {}) {
|
|
8123
|
+
return super.init(els, options, _RichTextEditor);
|
|
8124
|
+
}
|
|
8125
|
+
static getInstance(el) {
|
|
8126
|
+
return el["Expressive_RichTextEditor"];
|
|
8127
|
+
}
|
|
8128
|
+
destroy() {
|
|
8129
|
+
this.el["Expressive_RichTextEditor"] = void 0;
|
|
8130
|
+
if (!this.editor) return;
|
|
8131
|
+
this.el.removeEventListener("click", this._handleClick);
|
|
8132
|
+
this._label?.removeEventListener("click", this._handleLabelClick);
|
|
8133
|
+
this.editor.off("update", this._handleUpdate);
|
|
8134
|
+
this.editor.off("transaction", this._handleTransaction);
|
|
8135
|
+
this.editor.destroy();
|
|
8136
|
+
this.editor = null;
|
|
8137
|
+
this.content.remove();
|
|
8138
|
+
this.textarea.hidden = false;
|
|
8139
|
+
for (const button of this._actions.keys()) {
|
|
8140
|
+
button.disabled = false;
|
|
8141
|
+
button.classList.remove("active");
|
|
8142
|
+
button.removeAttribute("aria-pressed");
|
|
8143
|
+
}
|
|
8144
|
+
}
|
|
8145
|
+
};
|
|
8146
|
+
|
|
8147
|
+
// src/ts/components/mapView.ts
|
|
8148
|
+
var OPENFREEMAP = "https://tiles.openfreemap.org/styles/";
|
|
8149
|
+
var _defaults22 = {
|
|
8150
|
+
maplibregl: null,
|
|
8151
|
+
style: { light: `${OPENFREEMAP}positron`, dark: `${OPENFREEMAP}dark` },
|
|
8152
|
+
center: [0, 20],
|
|
8153
|
+
zoom: 1,
|
|
8154
|
+
mapOptions: {}
|
|
8155
|
+
};
|
|
8156
|
+
var lngLat = (el) => el.dataset.lngLat.split(",").map(Number);
|
|
8157
|
+
function arc([x0, y0], [x2, y2], steps = 32) {
|
|
8158
|
+
const bow = x2 >= x0 ? 0.25 : -0.25;
|
|
8159
|
+
const x1 = (x0 + x2) / 2 - (y2 - y0) * bow;
|
|
8160
|
+
const y1 = (y0 + y2) / 2 + (x2 - x0) * bow;
|
|
8161
|
+
return Array.from({ length: steps + 1 }, (_, i) => {
|
|
8162
|
+
const t = i / steps, u = 1 - t;
|
|
8163
|
+
return [u * u * x0 + 2 * u * t * x1 + t * t * x2, u * u * y0 + 2 * u * t * y1 + t * t * y2];
|
|
8164
|
+
});
|
|
8165
|
+
}
|
|
8166
|
+
var MapView = class _MapView extends Component {
|
|
8167
|
+
constructor(el, options) {
|
|
8168
|
+
super(el, options, _MapView);
|
|
8169
|
+
/** The MapLibre map. */
|
|
8170
|
+
this.map = null;
|
|
8171
|
+
/** The MapLibre markers made from `.map-marker` children, in order. */
|
|
8172
|
+
this.markers = [];
|
|
8173
|
+
this._children = [];
|
|
8174
|
+
this._popups = [];
|
|
8175
|
+
this._markerPopups = /* @__PURE__ */ new Map();
|
|
8176
|
+
this._overlays = /* @__PURE__ */ new Map();
|
|
8177
|
+
this._styleReady = false;
|
|
8178
|
+
this._dark = false;
|
|
8179
|
+
this._location = null;
|
|
8180
|
+
this._media = null;
|
|
8181
|
+
this._observer = null;
|
|
8182
|
+
// Looked up once: `move` fires every frame of a pan or zoom.
|
|
8183
|
+
this._zoomIn = [];
|
|
8184
|
+
this._zoomOut = [];
|
|
8185
|
+
this._handleStyleLoad = () => {
|
|
8186
|
+
this._styleReady = true;
|
|
8187
|
+
for (const overlay2 of this._overlays.values()) overlay2.draw();
|
|
8188
|
+
};
|
|
8189
|
+
this._handleScheme = () => {
|
|
8190
|
+
const dark = this._isDark();
|
|
8191
|
+
if (dark === this._dark) return;
|
|
8192
|
+
this._dark = dark;
|
|
8193
|
+
this._styleReady = false;
|
|
8194
|
+
this.map.setStyle(this._style(), { diff: false });
|
|
8195
|
+
};
|
|
8196
|
+
this._sync = () => {
|
|
8197
|
+
const zoom = this.map.getZoom();
|
|
8198
|
+
for (const button of this._zoomIn) button.disabled = zoom >= this.map.getMaxZoom();
|
|
8199
|
+
for (const button of this._zoomOut) button.disabled = zoom <= this.map.getMinZoom();
|
|
8200
|
+
this.el.style.setProperty("--md-comp-map-bearing", `${-this.map.getBearing()}deg`);
|
|
8201
|
+
};
|
|
8202
|
+
this._handleFullscreen = () => {
|
|
8203
|
+
for (const button of this._buttons("fullscreen")) {
|
|
8204
|
+
button.setAttribute("aria-pressed", String(document.fullscreenElement === this.el));
|
|
8205
|
+
}
|
|
8206
|
+
};
|
|
8207
|
+
this._handleClick = (e) => {
|
|
8208
|
+
const target = e.target;
|
|
8209
|
+
const button = target.closest(".map-controls [data-action]");
|
|
8210
|
+
const action = button?.dataset.action;
|
|
8211
|
+
if (action === "zoom-in") this.map.zoomIn();
|
|
8212
|
+
else if (action === "zoom-out") this.map.zoomOut();
|
|
8213
|
+
else if (action === "compass") this.map.resetNorthPitch();
|
|
8214
|
+
else if (action === "locate") this._locate(button);
|
|
8215
|
+
else if (action === "fullscreen") {
|
|
8216
|
+
if (document.fullscreenElement === this.el) document.exitFullscreen();
|
|
8217
|
+
else this.el.requestFullscreen();
|
|
8218
|
+
} else {
|
|
8219
|
+
const marker = target.closest(".map-marker");
|
|
8220
|
+
if (this._markerPopups.has(marker)) this._toggle(marker);
|
|
8221
|
+
}
|
|
8222
|
+
};
|
|
8223
|
+
// A click on the map closes marker popups, but not the one being toggled.
|
|
8224
|
+
this._handleMapClick = (e) => {
|
|
8225
|
+
const target = e.originalEvent.target;
|
|
8226
|
+
for (const [marker, popup] of this._markerPopups) {
|
|
8227
|
+
if (!marker.contains(target) && !popup.getElement()?.contains(target)) popup.remove();
|
|
8228
|
+
}
|
|
8229
|
+
};
|
|
8230
|
+
this._handleKeydown = (e) => {
|
|
8231
|
+
if (e.key !== "Escape") return;
|
|
8232
|
+
for (const popup of this._popups) {
|
|
8233
|
+
if (popup.getElement()?.contains(e.target)) {
|
|
8234
|
+
e.preventDefault();
|
|
8235
|
+
popup.remove();
|
|
8236
|
+
}
|
|
8237
|
+
}
|
|
8238
|
+
};
|
|
8239
|
+
this.el["Expressive_MapView"] = this;
|
|
8240
|
+
this.options = {
|
|
8241
|
+
..._MapView.defaults,
|
|
8242
|
+
...options
|
|
8243
|
+
};
|
|
8244
|
+
const lib = this.options.maplibregl;
|
|
8245
|
+
if (!lib) {
|
|
8246
|
+
console.error("MapView needs the maplibre-gl module in options.maplibregl.");
|
|
8247
|
+
return;
|
|
8248
|
+
}
|
|
8249
|
+
this._children = [...el.children];
|
|
8250
|
+
this._zoomIn = [...this._buttons("zoom-in")];
|
|
8251
|
+
this._zoomOut = [...this._buttons("zoom-out")];
|
|
8252
|
+
this._dark = this._isDark();
|
|
8253
|
+
this.map = new lib.Map({
|
|
8254
|
+
...this.options.mapOptions,
|
|
8255
|
+
container: el,
|
|
8256
|
+
style: this._style(),
|
|
8257
|
+
center: this.options.center,
|
|
8258
|
+
zoom: this.options.zoom
|
|
8259
|
+
});
|
|
8260
|
+
this.map.on("style.load", this._handleStyleLoad);
|
|
8261
|
+
this.map.on("move", this._sync);
|
|
8262
|
+
this.map.on("click", this._handleMapClick);
|
|
8263
|
+
const children = this._children.filter((child) => child instanceof HTMLElement);
|
|
8264
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
8265
|
+
for (const marker of children.filter((child) => child.matches(".map-marker[data-lng-lat]"))) {
|
|
8266
|
+
const target = marker.hasAttribute("aria-controls") ? Utils.getElementById(marker, marker.getAttribute("aria-controls")) : null;
|
|
8267
|
+
const popup = target?.parentElement === el && target.matches(".map-popup") ? target : null;
|
|
8268
|
+
if (popup) claimed.add(popup);
|
|
8269
|
+
this._addMarker(marker, popup);
|
|
8270
|
+
}
|
|
8271
|
+
for (const popup of children.filter((child) => child.matches(".map-popup[data-lng-lat]") && !claimed.has(child))) {
|
|
8272
|
+
this._popup(popup, 0).setLngLat(lngLat(popup)).addTo(this.map);
|
|
8273
|
+
}
|
|
8274
|
+
if (this._themed()) {
|
|
8275
|
+
this._media = matchMedia("(prefers-color-scheme: dark)");
|
|
8276
|
+
this._media.addEventListener("change", this._handleScheme);
|
|
8277
|
+
this._observer = new MutationObserver(this._handleScheme);
|
|
8278
|
+
this._observer.observe(document.documentElement, { attributes: true, attributeFilter: ["theme"], subtree: true });
|
|
8279
|
+
}
|
|
8280
|
+
for (const button of this._buttons("fullscreen")) button.hidden = !document.fullscreenEnabled;
|
|
8281
|
+
document.addEventListener("fullscreenchange", this._handleFullscreen);
|
|
8282
|
+
el.addEventListener("click", this._handleClick);
|
|
8283
|
+
el.addEventListener("keydown", this._handleKeydown);
|
|
8284
|
+
this._sync();
|
|
8285
|
+
}
|
|
8286
|
+
static get defaults() {
|
|
8287
|
+
return _defaults22;
|
|
8288
|
+
}
|
|
8289
|
+
/**
|
|
8290
|
+
* Initializes instances of MapView.
|
|
8291
|
+
* @param els HTML elements.
|
|
8292
|
+
* @param options Component options.
|
|
8293
|
+
*/
|
|
8294
|
+
static init(els, options = {}) {
|
|
8295
|
+
return super.init(els, options, _MapView);
|
|
8296
|
+
}
|
|
8297
|
+
static getInstance(el) {
|
|
8298
|
+
return el["Expressive_MapView"];
|
|
8299
|
+
}
|
|
8300
|
+
destroy() {
|
|
8301
|
+
this.el["Expressive_MapView"] = void 0;
|
|
8302
|
+
if (!this.map) return;
|
|
8303
|
+
this.el.removeEventListener("click", this._handleClick);
|
|
8304
|
+
this.el.removeEventListener("keydown", this._handleKeydown);
|
|
8305
|
+
document.removeEventListener("fullscreenchange", this._handleFullscreen);
|
|
8306
|
+
this._media?.removeEventListener("change", this._handleScheme);
|
|
8307
|
+
this._observer?.disconnect();
|
|
8308
|
+
for (const overlay2 of this._overlays.values()) overlay2.off?.();
|
|
8309
|
+
for (const popup of this._popups) popup.remove();
|
|
8310
|
+
for (const marker of this.markers) {
|
|
8311
|
+
marker.remove();
|
|
8312
|
+
const element = marker.getElement();
|
|
8313
|
+
element.classList.remove(...[...element.classList].filter((name) => name.startsWith("maplibregl-")));
|
|
8314
|
+
element.style.removeProperty("transform");
|
|
8315
|
+
element.style.removeProperty("opacity");
|
|
8316
|
+
if (element.hasAttribute("aria-controls")) element.removeAttribute("aria-expanded");
|
|
8317
|
+
}
|
|
8318
|
+
this._location?.remove();
|
|
8319
|
+
this._location = null;
|
|
8320
|
+
this.map.remove();
|
|
8321
|
+
this.map = null;
|
|
8322
|
+
this.markers = [];
|
|
8323
|
+
this._popups = [];
|
|
8324
|
+
this._markerPopups.clear();
|
|
8325
|
+
this._overlays.clear();
|
|
8326
|
+
this.el.style.removeProperty("--md-comp-map-bearing");
|
|
8327
|
+
for (const button of this.el.querySelectorAll(".map-controls [data-action]")) {
|
|
8328
|
+
button.disabled = false;
|
|
8329
|
+
button.hidden = false;
|
|
8330
|
+
button.removeAttribute("aria-pressed");
|
|
8331
|
+
}
|
|
8332
|
+
this.el.append(...this._children);
|
|
8333
|
+
}
|
|
8334
|
+
/** Draws a line through `[longitude, latitude]` points. Replaces an overlay with the same id. */
|
|
8335
|
+
addRoute(id, coordinates, options = {}) {
|
|
8336
|
+
this._line(id, { type: "LineString", coordinates }, "route", { width: 4, ...options });
|
|
8337
|
+
}
|
|
8338
|
+
/** Draws a curved line for each `{ from, to }`. Replaces an overlay with the same id. */
|
|
8339
|
+
addArcs(id, arcs, options = {}) {
|
|
8340
|
+
this._line(id, { type: "MultiLineString", coordinates: arcs.map(({ from, to }) => arc(from, to)) }, "arc", { width: 2, ...options });
|
|
8341
|
+
}
|
|
8342
|
+
/**
|
|
8343
|
+
* Draws GeoJSON points, or the URL of a GeoJSON file, as clusters that
|
|
8344
|
+
* show their count and zoom in when clicked. The layers are
|
|
8345
|
+
* `${id}-clusters`, `${id}-count` and `${id}-points`.
|
|
8346
|
+
*/
|
|
8347
|
+
addClusters(id, data, options = {}) {
|
|
8348
|
+
const clusters = `${id}-clusters`, points = `${id}-points`;
|
|
8349
|
+
const zoomIn = async (e) => {
|
|
8350
|
+
const [feature] = e.features;
|
|
8351
|
+
const zoom = await this.map.getSource(id).getClusterExpansionZoom(feature.properties.cluster_id);
|
|
8352
|
+
this.map.easeTo({ center: feature.geometry.coordinates, zoom });
|
|
8353
|
+
};
|
|
8354
|
+
const pointer = () => {
|
|
8355
|
+
this.map.getCanvas().style.cursor = "pointer";
|
|
8356
|
+
};
|
|
8357
|
+
const reset = () => {
|
|
8358
|
+
this.map.getCanvas().style.cursor = "";
|
|
8359
|
+
};
|
|
8360
|
+
this._draw(id, {
|
|
8361
|
+
draw: () => {
|
|
8362
|
+
const color = options.color ?? this._color("var(--md-comp-map-cluster-color)");
|
|
8363
|
+
const surface = this._color("var(--md-comp-map-marker-ring-color)");
|
|
8364
|
+
this.map.addSource(id, {
|
|
8365
|
+
type: "geojson",
|
|
8366
|
+
data,
|
|
8367
|
+
cluster: true,
|
|
8368
|
+
clusterRadius: options.radius ?? 50,
|
|
8369
|
+
clusterMaxZoom: options.maxZoom ?? 14
|
|
8370
|
+
});
|
|
8371
|
+
this.map.addLayer({
|
|
8372
|
+
id: clusters,
|
|
8373
|
+
type: "circle",
|
|
8374
|
+
source: id,
|
|
8375
|
+
filter: ["has", "point_count"],
|
|
8376
|
+
paint: {
|
|
8377
|
+
"circle-color": color,
|
|
8378
|
+
"circle-radius": ["step", ["get", "point_count"], 16, 10, 20, 100, 26],
|
|
8379
|
+
"circle-stroke-width": 6,
|
|
8380
|
+
"circle-stroke-color": color,
|
|
8381
|
+
"circle-stroke-opacity": 0.3
|
|
8382
|
+
}
|
|
8383
|
+
});
|
|
8384
|
+
const style = this.map.getStyle();
|
|
8385
|
+
const font = style.glyphs && style.layers.find((layer) => layer.layout?.["text-font"])?.layout["text-font"];
|
|
8386
|
+
if (font) {
|
|
8387
|
+
this.map.addLayer({
|
|
8388
|
+
id: `${id}-count`,
|
|
8389
|
+
type: "symbol",
|
|
8390
|
+
source: id,
|
|
8391
|
+
filter: ["has", "point_count"],
|
|
8392
|
+
layout: { "text-field": ["get", "point_count_abbreviated"], "text-font": font, "text-size": 12 },
|
|
8393
|
+
paint: { "text-color": this._color("var(--md-comp-map-cluster-label-color)") }
|
|
8394
|
+
});
|
|
8395
|
+
}
|
|
8396
|
+
this.map.addLayer({
|
|
8397
|
+
id: points,
|
|
8398
|
+
type: "circle",
|
|
8399
|
+
source: id,
|
|
8400
|
+
filter: ["!", ["has", "point_count"]],
|
|
8401
|
+
paint: { "circle-color": color, "circle-radius": 6, "circle-stroke-width": 2, "circle-stroke-color": surface }
|
|
8402
|
+
});
|
|
8403
|
+
},
|
|
8404
|
+
off: () => {
|
|
8405
|
+
this.map.off("click", clusters, zoomIn);
|
|
8406
|
+
for (const layer of [clusters, points]) {
|
|
8407
|
+
this.map.off("mouseenter", layer, pointer);
|
|
8408
|
+
this.map.off("mouseleave", layer, reset);
|
|
8409
|
+
}
|
|
8410
|
+
}
|
|
8411
|
+
});
|
|
8412
|
+
this.map.on("click", clusters, zoomIn);
|
|
8413
|
+
for (const layer of [clusters, points]) {
|
|
8414
|
+
this.map.on("mouseenter", layer, pointer);
|
|
8415
|
+
this.map.on("mouseleave", layer, reset);
|
|
8416
|
+
}
|
|
8417
|
+
}
|
|
8418
|
+
/** Removes a route, arcs or clusters added with this id. */
|
|
8419
|
+
removeOverlay(id) {
|
|
8420
|
+
const overlay2 = this._overlays.get(id);
|
|
8421
|
+
if (!overlay2) return;
|
|
8422
|
+
this._overlays.delete(id);
|
|
8423
|
+
overlay2.off?.();
|
|
8424
|
+
if (!this._styleReady || !this.map.getSource(id)) return;
|
|
8425
|
+
for (const layer of this.map.getStyle().layers) if (layer.source === id) this.map.removeLayer(layer.id);
|
|
8426
|
+
this.map.removeSource(id);
|
|
8427
|
+
}
|
|
8428
|
+
_line(id, geometry, token, options) {
|
|
8429
|
+
this._draw(id, {
|
|
8430
|
+
draw: () => {
|
|
8431
|
+
this.map.addSource(id, { type: "geojson", data: { type: "Feature", properties: {}, geometry } });
|
|
8432
|
+
this.map.addLayer({
|
|
8433
|
+
id,
|
|
8434
|
+
type: "line",
|
|
8435
|
+
source: id,
|
|
8436
|
+
layout: { "line-cap": "round", "line-join": "round" },
|
|
8437
|
+
paint: {
|
|
8438
|
+
"line-color": options.color ?? this._color(`var(--md-comp-map-${token}-color)`),
|
|
8439
|
+
"line-width": options.width,
|
|
8440
|
+
"line-opacity": options.opacity ?? 1,
|
|
8441
|
+
...options.dashArray && { "line-dasharray": options.dashArray }
|
|
8442
|
+
}
|
|
8443
|
+
});
|
|
8444
|
+
}
|
|
8445
|
+
});
|
|
8446
|
+
}
|
|
8447
|
+
// Sources and layers go with the style, so every overlay is drawn again
|
|
8448
|
+
// when a theme change swaps it.
|
|
8449
|
+
_draw(id, overlay2) {
|
|
8450
|
+
this.removeOverlay(id);
|
|
8451
|
+
this._overlays.set(id, overlay2);
|
|
8452
|
+
if (this._styleReady) overlay2.draw();
|
|
8453
|
+
}
|
|
8454
|
+
_addMarker(element, content) {
|
|
8455
|
+
const marker = new this.options.maplibregl.Marker({
|
|
8456
|
+
element,
|
|
8457
|
+
draggable: element.hasAttribute("data-draggable")
|
|
8458
|
+
}).setLngLat(lngLat(element)).addTo(this.map);
|
|
8459
|
+
marker.on("dragend", () => {
|
|
8460
|
+
const { lng, lat } = marker.getLngLat();
|
|
8461
|
+
element.dataset.lngLat = `${lng},${lat}`;
|
|
8462
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
8463
|
+
});
|
|
8464
|
+
this.markers.push(marker);
|
|
8465
|
+
if (!content) return;
|
|
8466
|
+
element.setAttribute("aria-expanded", "false");
|
|
8467
|
+
const popup = this._popup(content, 16);
|
|
8468
|
+
popup.on("open", () => element.setAttribute("aria-expanded", "true"));
|
|
8469
|
+
popup.on("close", () => {
|
|
8470
|
+
element.setAttribute("aria-expanded", "false");
|
|
8471
|
+
if (!document.activeElement || document.activeElement === document.body) element.focus();
|
|
8472
|
+
});
|
|
8473
|
+
this._markerPopups.set(element, popup);
|
|
8474
|
+
}
|
|
8475
|
+
_popup(content, offset) {
|
|
8476
|
+
const popup = new this.options.maplibregl.Popup({ closeOnClick: false, maxWidth: "none", offset }).setDOMContent(content);
|
|
8477
|
+
this._popups.push(popup);
|
|
8478
|
+
return popup;
|
|
8479
|
+
}
|
|
8480
|
+
_toggle(marker) {
|
|
8481
|
+
const popup = this._markerPopups.get(marker);
|
|
8482
|
+
if (popup.isOpen()) popup.remove();
|
|
8483
|
+
else popup.setLngLat(this.markers.find((m) => m.getElement() === marker).getLngLat()).addTo(this.map);
|
|
8484
|
+
}
|
|
8485
|
+
_buttons(action) {
|
|
8486
|
+
return this.el.querySelectorAll(`.map-controls [data-action="${action}"]`);
|
|
8487
|
+
}
|
|
8488
|
+
_locate(button) {
|
|
8489
|
+
navigator.geolocation?.getCurrentPosition(
|
|
8490
|
+
({ coords }) => {
|
|
8491
|
+
if (!this.map) return;
|
|
8492
|
+
const center = [coords.longitude, coords.latitude];
|
|
8493
|
+
if (!this._location) {
|
|
8494
|
+
const dot = document.createElement("span");
|
|
8495
|
+
dot.className = "map-marker location";
|
|
8496
|
+
dot.setAttribute("aria-hidden", "true");
|
|
8497
|
+
this._location = new this.options.maplibregl.Marker({ element: dot });
|
|
8498
|
+
}
|
|
8499
|
+
this._location.setLngLat(center).addTo(this.map);
|
|
8500
|
+
this.map.flyTo({ center, zoom: Math.max(this.map.getZoom(), 14) });
|
|
8501
|
+
},
|
|
8502
|
+
(error) => {
|
|
8503
|
+
if (this.map && error.code === error.PERMISSION_DENIED) button.disabled = true;
|
|
8504
|
+
}
|
|
8505
|
+
);
|
|
8506
|
+
}
|
|
8507
|
+
// Resolves a CSS color where the map is, so light-dark() and scoped
|
|
8508
|
+
// themes apply, then paints it on a canvas to read back the sRGB value.
|
|
8509
|
+
// MapLibre does not parse the oklch() the theme tokens compute to.
|
|
8510
|
+
_color(value) {
|
|
8511
|
+
const probe = document.createElement("span");
|
|
8512
|
+
probe.hidden = true;
|
|
8513
|
+
probe.style.color = value;
|
|
8514
|
+
this.el.append(probe);
|
|
8515
|
+
const context = document.createElement("canvas").getContext("2d", { willReadFrequently: true });
|
|
8516
|
+
context.fillStyle = getComputedStyle(probe).color;
|
|
8517
|
+
probe.remove();
|
|
8518
|
+
context.fillRect(0, 0, 1, 1);
|
|
8519
|
+
const [r, g, b, a] = context.getImageData(0, 0, 1, 1).data;
|
|
8520
|
+
return `rgba(${r}, ${g}, ${b}, ${Math.round(a / 255 * 100) / 100})`;
|
|
8521
|
+
}
|
|
8522
|
+
_isDark() {
|
|
8523
|
+
return this._color("light-dark(black, white)") === "rgba(255, 255, 255, 1)";
|
|
8524
|
+
}
|
|
8525
|
+
_themed() {
|
|
8526
|
+
const style = this.options.style;
|
|
8527
|
+
return typeof style === "object" && "light" in style && "dark" in style;
|
|
8528
|
+
}
|
|
8529
|
+
_style() {
|
|
8530
|
+
return this._themed() ? this.options.style[this._dark ? "dark" : "light"] : this.options.style;
|
|
8531
|
+
}
|
|
8532
|
+
};
|
|
8533
|
+
|
|
5598
8534
|
// src/ts/components/lightbox.ts
|
|
5599
|
-
var
|
|
8535
|
+
var _defaults23 = {
|
|
5600
8536
|
inDuration: 275,
|
|
5601
8537
|
outDuration: 200,
|
|
5602
8538
|
onOpenStart: null,
|
|
@@ -5745,7 +8681,7 @@ var _Lightbox = class _Lightbox extends Component {
|
|
|
5745
8681
|
this._setupEventHandlers();
|
|
5746
8682
|
}
|
|
5747
8683
|
static get defaults() {
|
|
5748
|
-
return
|
|
8684
|
+
return _defaults23;
|
|
5749
8685
|
}
|
|
5750
8686
|
/**
|
|
5751
8687
|
* Initializes instances of Lightbox.
|
|
@@ -5951,7 +8887,7 @@ var Lightbox = _Lightbox;
|
|
|
5951
8887
|
|
|
5952
8888
|
// src/ts/components/scrollspy.ts
|
|
5953
8889
|
var _defaultGetActiveElement = (id) => `a[href="#${id}"]`;
|
|
5954
|
-
var
|
|
8890
|
+
var _defaults24 = {
|
|
5955
8891
|
scrollOffset: 200,
|
|
5956
8892
|
activeClass: "active",
|
|
5957
8893
|
getActiveElement: _defaultGetActiveElement,
|
|
@@ -5987,7 +8923,7 @@ var _ScrollSpy = class _ScrollSpy extends Component {
|
|
|
5987
8923
|
}
|
|
5988
8924
|
}
|
|
5989
8925
|
static get defaults() {
|
|
5990
|
-
return
|
|
8926
|
+
return _defaults24;
|
|
5991
8927
|
}
|
|
5992
8928
|
static init(els, options = {}) {
|
|
5993
8929
|
return super.init(els, options, _ScrollSpy);
|
|
@@ -6021,8 +8957,9 @@ var _ScrollSpy = class _ScrollSpy extends Component {
|
|
|
6021
8957
|
const retained = elements.filter((spy) => spy.options.keepTopElementActive);
|
|
6022
8958
|
best = _ScrollSpy._nearestAbove(retained) ?? retained[0];
|
|
6023
8959
|
}
|
|
6024
|
-
const next = best ? _ScrollSpy._linkFor(best.el.id, best.options) : null;
|
|
6025
8960
|
const active = _ScrollSpy._active.get(resolver);
|
|
8961
|
+
if (best && active === best && best._link?.isConnected) return;
|
|
8962
|
+
const next = best ? _ScrollSpy._linkFor(best.el.id, best.options) : null;
|
|
6026
8963
|
if (active === best && active?._link === next) return;
|
|
6027
8964
|
_ScrollSpy._clearActive(resolver);
|
|
6028
8965
|
if (!next) return;
|
|
@@ -6077,7 +9014,7 @@ _ScrollSpy._active = /* @__PURE__ */ new Map();
|
|
|
6077
9014
|
var ScrollSpy = _ScrollSpy;
|
|
6078
9015
|
|
|
6079
9016
|
// src/ts/components/navigationRail.ts
|
|
6080
|
-
var
|
|
9017
|
+
var _defaults25 = {
|
|
6081
9018
|
onOpenStart: null,
|
|
6082
9019
|
onOpenEnd: null,
|
|
6083
9020
|
onCloseStart: null,
|
|
@@ -6164,7 +9101,7 @@ var NavigationRail = class _NavigationRail extends Component {
|
|
|
6164
9101
|
document.addEventListener("keydown", this._onKeyDown);
|
|
6165
9102
|
}
|
|
6166
9103
|
static get defaults() {
|
|
6167
|
-
return
|
|
9104
|
+
return _defaults25;
|
|
6168
9105
|
}
|
|
6169
9106
|
static init(els, options = {}) {
|
|
6170
9107
|
return super.init(els, options, _NavigationRail);
|
|
@@ -6194,7 +9131,7 @@ var NavigationRail = class _NavigationRail extends Component {
|
|
|
6194
9131
|
};
|
|
6195
9132
|
|
|
6196
9133
|
// src/ts/components/tabs.ts
|
|
6197
|
-
var
|
|
9134
|
+
var _defaults26 = {
|
|
6198
9135
|
duration: 300,
|
|
6199
9136
|
onShow: null,
|
|
6200
9137
|
swipeable: false,
|
|
@@ -6207,13 +9144,17 @@ var Tabs = class _Tabs extends Component {
|
|
|
6207
9144
|
super(el, options, _Tabs);
|
|
6208
9145
|
this._originalPanels = /* @__PURE__ */ new Map();
|
|
6209
9146
|
this._originalLinks = /* @__PURE__ */ new Map();
|
|
6210
|
-
|
|
9147
|
+
// Throttled like Carousel's: every bar re-measures on resize, and the left
|
|
9148
|
+
// and right positions are both read before either is written.
|
|
9149
|
+
this._handleWindowResize = Utils.throttle(() => {
|
|
6211
9150
|
this._setTabsAndTabWidth();
|
|
6212
9151
|
if (this._tabWidth !== 0 && this._tabsWidth !== 0) {
|
|
6213
|
-
|
|
6214
|
-
|
|
9152
|
+
const left = this._calcLeftPos(this._activeTabLink);
|
|
9153
|
+
const right = this._calcRightPos(this._activeTabLink);
|
|
9154
|
+
this._indicator.style.left = left + "px";
|
|
9155
|
+
this._indicator.style.right = right + "px";
|
|
6215
9156
|
}
|
|
6216
|
-
};
|
|
9157
|
+
}, 200);
|
|
6217
9158
|
this._handleTabClick = (e) => {
|
|
6218
9159
|
const target = e.target;
|
|
6219
9160
|
if (!target) return;
|
|
@@ -6281,7 +9222,7 @@ var Tabs = class _Tabs extends Component {
|
|
|
6281
9222
|
this._setupEventHandlers();
|
|
6282
9223
|
}
|
|
6283
9224
|
static get defaults() {
|
|
6284
|
-
return
|
|
9225
|
+
return _defaults26;
|
|
6285
9226
|
}
|
|
6286
9227
|
/**
|
|
6287
9228
|
* Initializes instances of Tabs.
|
|
@@ -6468,7 +9409,7 @@ var Tabs = class _Tabs extends Component {
|
|
|
6468
9409
|
};
|
|
6469
9410
|
|
|
6470
9411
|
// src/ts/components/timepicker.ts
|
|
6471
|
-
var
|
|
9412
|
+
var _defaults27 = {
|
|
6472
9413
|
dialRadius: 135,
|
|
6473
9414
|
outerRadius: 105,
|
|
6474
9415
|
innerRadius: 70,
|
|
@@ -6664,7 +9605,7 @@ var _Timepicker = class _Timepicker extends Component {
|
|
|
6664
9605
|
}
|
|
6665
9606
|
}
|
|
6666
9607
|
static get defaults() {
|
|
6667
|
-
return
|
|
9608
|
+
return _defaults27;
|
|
6668
9609
|
}
|
|
6669
9610
|
/**
|
|
6670
9611
|
* Initializes instances of Timepicker.
|
|
@@ -7026,7 +9967,7 @@ _Timepicker._template = `<div class="timepicker-container">
|
|
|
7026
9967
|
var Timepicker = _Timepicker;
|
|
7027
9968
|
|
|
7028
9969
|
// src/ts/components/snackbar.ts
|
|
7029
|
-
var
|
|
9970
|
+
var _defaults28 = {
|
|
7030
9971
|
text: "",
|
|
7031
9972
|
action: "",
|
|
7032
9973
|
onAction: null,
|
|
@@ -7076,7 +10017,7 @@ var _Snackbar = class _Snackbar {
|
|
|
7076
10017
|
this._setTimer();
|
|
7077
10018
|
}
|
|
7078
10019
|
static get defaults() {
|
|
7079
|
-
return
|
|
10020
|
+
return _defaults28;
|
|
7080
10021
|
}
|
|
7081
10022
|
static getInstance(el) {
|
|
7082
10023
|
return el["Expressive_Snackbar"];
|
|
@@ -7116,6 +10057,7 @@ var _Snackbar = class _Snackbar {
|
|
|
7116
10057
|
snackbar.velocityX = 0;
|
|
7117
10058
|
snackbar.time = Date.now();
|
|
7118
10059
|
snackbar.xPos = e.clientX;
|
|
10060
|
+
snackbar._activationDistance = snackbar.el.offsetWidth * snackbar.options.activationPercent;
|
|
7119
10061
|
}
|
|
7120
10062
|
}
|
|
7121
10063
|
static _onDragMove(e) {
|
|
@@ -7128,7 +10070,7 @@ var _Snackbar = class _Snackbar {
|
|
|
7128
10070
|
snackbar.velocityX = snackbar.deltaX / (Date.now() - snackbar.time);
|
|
7129
10071
|
snackbar.time = Date.now();
|
|
7130
10072
|
const totalDeltaX = snackbar.xPos - snackbar.startingXPos;
|
|
7131
|
-
const activationDistance = snackbar.
|
|
10073
|
+
const activationDistance = snackbar._activationDistance;
|
|
7132
10074
|
snackbar.el.style.transform = `translateX(${totalDeltaX}px)`;
|
|
7133
10075
|
snackbar.el.style.opacity = (1 - Math.abs(totalDeltaX / activationDistance)).toString();
|
|
7134
10076
|
}
|
|
@@ -7141,7 +10083,7 @@ var _Snackbar = class _Snackbar {
|
|
|
7141
10083
|
snackbar.panning = false;
|
|
7142
10084
|
snackbar.el.classList.remove("panning");
|
|
7143
10085
|
const totalDeltaX = snackbar.xPos - snackbar.startingXPos;
|
|
7144
|
-
const activationDistance = snackbar.
|
|
10086
|
+
const activationDistance = snackbar._activationDistance;
|
|
7145
10087
|
const velocity = Date.now() - snackbar.time < 100 ? snackbar.velocityX : 0;
|
|
7146
10088
|
const shouldBeDismissed = Math.abs(totalDeltaX) > activationDistance || velocity > 1;
|
|
7147
10089
|
if (shouldBeDismissed) {
|
|
@@ -7373,7 +10315,7 @@ _Snackbar._draggedSnackbar = null;
|
|
|
7373
10315
|
var Snackbar = _Snackbar;
|
|
7374
10316
|
|
|
7375
10317
|
// src/ts/components/tooltip.ts
|
|
7376
|
-
var
|
|
10318
|
+
var _defaults29 = {
|
|
7377
10319
|
exitDelay: 200,
|
|
7378
10320
|
enterDelay: 0,
|
|
7379
10321
|
text: "",
|
|
@@ -7395,6 +10337,7 @@ var Tooltip = class _Tooltip extends Component {
|
|
|
7395
10337
|
clearTimeout(this._exitDelayTimeout);
|
|
7396
10338
|
clearTimeout(this._animationTimeout);
|
|
7397
10339
|
this.isOpen = true;
|
|
10340
|
+
this.el.ownerDocument.addEventListener("keydown", this._handleKeydown, true);
|
|
7398
10341
|
this.options = { ...this.options, ...this._getAttributeOptions() };
|
|
7399
10342
|
this._updateTooltipContent();
|
|
7400
10343
|
this._setEnterDelayTimeout(isManual);
|
|
@@ -7407,6 +10350,7 @@ var Tooltip = class _Tooltip extends Component {
|
|
|
7407
10350
|
this.isHovered = false;
|
|
7408
10351
|
this.isFocused = false;
|
|
7409
10352
|
this.isOpen = false;
|
|
10353
|
+
this.el.ownerDocument.removeEventListener("keydown", this._handleKeydown, true);
|
|
7410
10354
|
clearTimeout(this._enterDelayTimeout);
|
|
7411
10355
|
this._setExitDelayTimeout();
|
|
7412
10356
|
};
|
|
@@ -7455,7 +10399,7 @@ var Tooltip = class _Tooltip extends Component {
|
|
|
7455
10399
|
this._setupEventHandlers();
|
|
7456
10400
|
}
|
|
7457
10401
|
static get defaults() {
|
|
7458
|
-
return
|
|
10402
|
+
return _defaults29;
|
|
7459
10403
|
}
|
|
7460
10404
|
/**
|
|
7461
10405
|
* Initializes instances of Tooltip.
|
|
@@ -7536,7 +10480,6 @@ var Tooltip = class _Tooltip extends Component {
|
|
|
7536
10480
|
el.addEventListener("focus", this._handleFocus, true);
|
|
7537
10481
|
el.addEventListener("blur", this._handleBlur, true);
|
|
7538
10482
|
}
|
|
7539
|
-
this.el.ownerDocument.addEventListener("keydown", this._handleKeydown, true);
|
|
7540
10483
|
}
|
|
7541
10484
|
_removeEventHandlers() {
|
|
7542
10485
|
for (const el of [this.el, this.tooltipEl]) {
|
|
@@ -7674,11 +10617,33 @@ var AUTO_INIT_COMPONENTS = {
|
|
|
7674
10617
|
},
|
|
7675
10618
|
Carousel: { component: Carousel, selector: ".carousel" },
|
|
7676
10619
|
Chips: { component: Chips, selector: ".chips" },
|
|
10620
|
+
Confetti: { component: Confetti, selector: ".confetti" },
|
|
10621
|
+
Particles: { component: Particles, selector: "canvas.particles" },
|
|
10622
|
+
ColumnChart: { component: ColumnChart, selector: ".column-chart" },
|
|
10623
|
+
BarChart: { component: BarChart, selector: ".bar-chart" },
|
|
10624
|
+
PieChart: { component: PieChart, selector: ".pie-chart" },
|
|
10625
|
+
HeatmapChart: { component: HeatmapChart, selector: ".heatmap-chart" },
|
|
10626
|
+
RadarChart: { component: RadarChart, selector: ".radar-chart" },
|
|
10627
|
+
RadialChart: { component: RadialChart, selector: ".radial-chart" },
|
|
10628
|
+
MixedChart: { component: MixedChart, selector: ".mixed-chart" },
|
|
10629
|
+
CommandPalette: {
|
|
10630
|
+
component: CommandPalette,
|
|
10631
|
+
selector: "dialog.command-palette"
|
|
10632
|
+
},
|
|
10633
|
+
Questionnaire: {
|
|
10634
|
+
component: Questionnaire,
|
|
10635
|
+
selector: "form.questionnaire"
|
|
10636
|
+
},
|
|
7677
10637
|
Datepicker: {
|
|
7678
10638
|
component: Datepicker,
|
|
7679
10639
|
selector: ".datepicker, .date-picker"
|
|
7680
10640
|
},
|
|
10641
|
+
LineChart: { component: LineChart, selector: ".line-chart" },
|
|
7681
10642
|
Menu: { component: Menu, selector: ".menu-trigger" },
|
|
10643
|
+
MessageScroller: {
|
|
10644
|
+
component: MessageScroller,
|
|
10645
|
+
selector: ".message-scroller"
|
|
10646
|
+
},
|
|
7682
10647
|
Lightbox: { component: Lightbox, selector: ".lightboxed" },
|
|
7683
10648
|
ScrollSpy: { component: ScrollSpy, selector: ".scrollspy" },
|
|
7684
10649
|
FormSelect: { component: FormSelect, selector: "select" },
|
|
@@ -7712,7 +10677,7 @@ function AutoInit(context = document.body, options) {
|
|
|
7712
10677
|
}
|
|
7713
10678
|
|
|
7714
10679
|
// src/ts/modular.ts
|
|
7715
|
-
var version = "0.
|
|
10680
|
+
var version = "0.14.1";
|
|
7716
10681
|
|
|
7717
10682
|
// src/ts/index.ts
|
|
7718
10683
|
Forms.Init();
|