@expressivecss/expressive 0.12.0 → 0.13.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 +231 -1
- package/README.md +37 -1
- package/dist/css/expressive.css +5111 -36
- 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-HLDXZ7XB.mjs → chunk-3CLXLPYS.mjs} +2 -2
- package/dist/js/chunks/chunk-3DHK5CZU.mjs +105 -0
- package/dist/js/chunks/chunk-3DHK5CZU.mjs.map +7 -0
- package/dist/js/chunks/chunk-4PWAPV6F.mjs +7 -0
- package/dist/js/chunks/chunk-4PWAPV6F.mjs.map +7 -0
- package/dist/js/chunks/{chunk-HB6IW7QQ.mjs → chunk-55IA2XUC.mjs} +2 -2
- package/dist/js/chunks/chunk-CCWDV5CR.mjs +292 -0
- package/dist/js/chunks/chunk-CCWDV5CR.mjs.map +7 -0
- package/dist/js/chunks/chunk-DTKR54I6.mjs +270 -0
- package/dist/js/chunks/chunk-DTKR54I6.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-HKT2S7VM.mjs +48 -0
- package/dist/js/chunks/chunk-HKT2S7VM.mjs.map +7 -0
- package/dist/js/chunks/chunk-K4CJCRGE.mjs +262 -0
- package/dist/js/chunks/chunk-K4CJCRGE.mjs.map +7 -0
- package/dist/js/chunks/chunk-LNCCYGPC.mjs +163 -0
- package/dist/js/chunks/chunk-LNCCYGPC.mjs.map +7 -0
- package/dist/js/chunks/chunk-NQLC3YMW.mjs +1 -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-P3G3QHII.mjs +166 -0
- package/dist/js/chunks/chunk-P3G3QHII.mjs.map +7 -0
- package/dist/js/chunks/{chunk-H2WA4XHH.mjs → chunk-PLYXLSH3.mjs} +4 -4
- package/dist/js/chunks/chunk-QKWPQK5N.mjs +179 -0
- package/dist/js/chunks/chunk-QKWPQK5N.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-SPETGK25.mjs +59 -0
- package/dist/js/chunks/chunk-SPETGK25.mjs.map +7 -0
- package/dist/js/chunks/{chunk-YSBCHYXP.mjs → chunk-U2OTHOTI.mjs} +116 -12
- package/dist/js/chunks/chunk-U2OTHOTI.mjs.map +7 -0
- package/dist/js/chunks/chunk-VTLBW7QS.mjs +124 -0
- package/dist/js/chunks/chunk-VTLBW7QS.mjs.map +7 -0
- package/dist/js/chunks/{chunk-RFZZCYVU.mjs → chunk-XGHORT7J.mjs} +2 -2
- package/dist/js/chunks/{chunk-HNFSMIBY.mjs → chunk-XH6BEEMY.mjs} +69 -16
- package/dist/js/chunks/chunk-XH6BEEMY.mjs.map +7 -0
- package/dist/js/expressive.cjs +2209 -32
- package/dist/js/expressive.cjs.map +4 -4
- package/dist/js/expressive.js +2209 -32
- package/dist/js/expressive.js.map +4 -4
- package/dist/js/expressive.min.js +10 -10
- package/dist/js/expressive.min.js.map +4 -4
- package/dist/js/expressive.mjs +68 -20
- package/dist/js/expressive.mjs.map +1 -1
- package/dist/js/internal/behaviors/forms.mjs +1 -1
- package/dist/js/internal/components/autocomplete.mjs +2 -2
- package/dist/js/internal/components/barChart.mjs +10 -0
- package/dist/js/internal/components/barChart.mjs.map +7 -0
- package/dist/js/internal/components/chips.mjs +3 -3
- 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/datepicker.mjs +3 -3
- 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 +65 -17
- package/dist/js/internal/components/lineChart.mjs +12 -0
- package/dist/js/internal/components/lineChart.mjs.map +7 -0
- package/dist/js/internal/components/menu.mjs +1 -1
- 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/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 +23 -11
- package/dist/js/internal/components/richTextEditor.mjs +9 -0
- package/dist/js/internal/components/richTextEditor.mjs.map +7 -0
- package/dist/js/internal/components/select.mjs +2 -2
- package/dist/js/modular.mjs +68 -20
- package/dist/types/behaviors/forms.d.ts +22 -0
- package/dist/types/components/barChart.d.ts +24 -0
- package/dist/types/components/columnChart.d.ts +21 -0
- package/dist/types/components/commandPalette.d.ts +96 -0
- package/dist/types/components/heatmapChart.d.ts +49 -0
- package/dist/types/components/index.d.ts +12 -0
- package/dist/types/components/lineChart.d.ts +134 -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/pieChart.d.ts +40 -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 +44 -0
- package/dist/types/components/richTextEditor.d.ts +71 -0
- package/dist/types/modular.d.ts +10 -1
- package/package.json +4 -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/_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/_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 +27 -0
- package/src/sass/components/_kpi.scss +162 -0
- package/src/sass/components/_line-chart.scss +102 -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/_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/_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/_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-G3I5H2U3.mjs +0 -7
- package/dist/js/chunks/chunk-G3I5H2U3.mjs.map +0 -7
- package/dist/js/chunks/chunk-HNFSMIBY.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-WWPAPHMA.mjs +0 -1
- package/dist/js/chunks/chunk-YSBCHYXP.mjs.map +0 -7
- /package/dist/js/chunks/{chunk-HLDXZ7XB.mjs.map → chunk-3CLXLPYS.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-HB6IW7QQ.mjs.map → chunk-55IA2XUC.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-WWPAPHMA.mjs.map → chunk-NQLC3YMW.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-H2WA4XHH.mjs.map → chunk-PLYXLSH3.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-RFZZCYVU.mjs.map → chunk-XGHORT7J.mjs.map} +0 -0
package/dist/js/expressive.cjs
CHANGED
|
@@ -22,22 +22,34 @@ __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,
|
|
31
34
|
Datepicker: () => Datepicker,
|
|
32
35
|
Dialogs: () => Dialogs,
|
|
33
36
|
ExpandingCard: () => ExpandingCard,
|
|
34
37
|
FloatingActionButton: () => FloatingActionButton,
|
|
35
38
|
FormSelect: () => FormSelect,
|
|
36
39
|
Forms: () => Forms,
|
|
40
|
+
HeatmapChart: () => HeatmapChart,
|
|
37
41
|
Lightbox: () => Lightbox,
|
|
42
|
+
LineChart: () => LineChart,
|
|
38
43
|
Menu: () => Menu,
|
|
44
|
+
MessageScroller: () => MessageScroller,
|
|
45
|
+
MixedChart: () => MixedChart,
|
|
39
46
|
NavigationRail: () => NavigationRail,
|
|
47
|
+
PieChart: () => PieChart,
|
|
48
|
+
Questionnaire: () => Questionnaire,
|
|
49
|
+
RadarChart: () => RadarChart,
|
|
50
|
+
RadialChart: () => RadialChart,
|
|
40
51
|
Range: () => Slider,
|
|
52
|
+
RichTextEditor: () => RichTextEditor,
|
|
41
53
|
ScrollSpy: () => ScrollSpy,
|
|
42
54
|
SideSheets: () => SideSheets,
|
|
43
55
|
Slider: () => Slider,
|
|
@@ -772,6 +784,7 @@ var _defaults3 = {
|
|
|
772
784
|
coverTrigger: false,
|
|
773
785
|
closeOnClick: true,
|
|
774
786
|
hover: false,
|
|
787
|
+
contextMenu: false,
|
|
775
788
|
inDuration: 150,
|
|
776
789
|
outDuration: 250,
|
|
777
790
|
onOpenStart: null,
|
|
@@ -787,6 +800,40 @@ var _Menu = class _Menu extends Component {
|
|
|
787
800
|
this._pendingTimers = /* @__PURE__ */ new Set();
|
|
788
801
|
this._transition = 0;
|
|
789
802
|
this._menuClickTargets = /* @__PURE__ */ new WeakMap();
|
|
803
|
+
/**
|
|
804
|
+
* Viewport point the open context menu belongs to. Null outside a
|
|
805
|
+
* right-click, so a keyboard or programmatic open uses the region's corner
|
|
806
|
+
* rather than an old pointer position.
|
|
807
|
+
*/
|
|
808
|
+
this._contextPoint = null;
|
|
809
|
+
/** What had focus when a context menu opened; focus goes back there. */
|
|
810
|
+
this._contextReturn = null;
|
|
811
|
+
this._handleContextMenu = (e) => {
|
|
812
|
+
if (e.defaultPrevented) {
|
|
813
|
+
this.close();
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const path = e.composedPath();
|
|
817
|
+
if (_Menu._menus.some((menu) => menu !== this && menu.isOpen && path.includes(menu.menuEl))) return;
|
|
818
|
+
e.preventDefault();
|
|
819
|
+
if (!this.isOpen) {
|
|
820
|
+
const root = this.el.getRootNode();
|
|
821
|
+
this._contextReturn = root.activeElement instanceof HTMLElement ? root.activeElement : null;
|
|
822
|
+
}
|
|
823
|
+
const rect = this.el.getBoundingClientRect();
|
|
824
|
+
const fromPointer = (e.clientX !== 0 || e.clientY !== 0) && e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
|
|
825
|
+
this._contextPoint = fromPointer ? { x: e.clientX, y: e.clientY } : null;
|
|
826
|
+
if (this.isOpen) this.recalculateDimensions();
|
|
827
|
+
else this.open();
|
|
828
|
+
};
|
|
829
|
+
this._handleDocumentContextMenu = (e) => {
|
|
830
|
+
const path = e.composedPath();
|
|
831
|
+
if (path.includes(this.menuEl)) {
|
|
832
|
+
e.preventDefault();
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (!path.includes(this.el)) this.close();
|
|
836
|
+
};
|
|
790
837
|
this._handleClick = (e) => {
|
|
791
838
|
e.preventDefault();
|
|
792
839
|
if (this.isOpen) {
|
|
@@ -833,6 +880,7 @@ var _Menu = class _Menu extends Component {
|
|
|
833
880
|
this.isTouchMoving = false;
|
|
834
881
|
};
|
|
835
882
|
this._handleTriggerKeydown = (e) => {
|
|
883
|
+
if (this.options.contextMenu) return;
|
|
836
884
|
const opensMenu = e.key === SPACE || e.key === Utils.keys.ENTER || e.key === Utils.keys.ARROW_DOWN || e.key === Utils.keys.ARROW_UP;
|
|
837
885
|
if (opensMenu && !this.isOpen) {
|
|
838
886
|
e.preventDefault();
|
|
@@ -991,7 +1039,7 @@ var _Menu = class _Menu extends Component {
|
|
|
991
1039
|
this._placeMenu();
|
|
992
1040
|
this._schedule(() => this._setupTemporaryEventHandlers(), 0);
|
|
993
1041
|
this._animateIn();
|
|
994
|
-
this.
|
|
1042
|
+
this._setExpanded(true);
|
|
995
1043
|
};
|
|
996
1044
|
/**
|
|
997
1045
|
* Close menu.
|
|
@@ -1013,10 +1061,13 @@ var _Menu = class _Menu extends Component {
|
|
|
1013
1061
|
if (transition !== this._transition) return;
|
|
1014
1062
|
this._animateOut();
|
|
1015
1063
|
this._removeTemporaryEventHandlers();
|
|
1016
|
-
this.
|
|
1064
|
+
this._setExpanded(false);
|
|
1017
1065
|
if (this.options.autoFocus && this.menuEl.contains(focused) && (root.activeElement === focused || this.el.ownerDocument.activeElement === this.el.ownerDocument.body)) {
|
|
1018
|
-
this.
|
|
1066
|
+
const back = this._contextReturn;
|
|
1067
|
+
(back?.isConnected && back !== this.el.ownerDocument.body ? back : this.el).focus();
|
|
1019
1068
|
}
|
|
1069
|
+
this._contextReturn = null;
|
|
1070
|
+
this._contextPoint = null;
|
|
1020
1071
|
};
|
|
1021
1072
|
/**
|
|
1022
1073
|
* While menu is open, you can recalculate its dimensions if its contents have changed.
|
|
@@ -1034,6 +1085,7 @@ var _Menu = class _Menu extends Component {
|
|
|
1034
1085
|
this._originalInert = this.menuEl?.getAttribute("inert") ?? null;
|
|
1035
1086
|
this.options = {
|
|
1036
1087
|
..._Menu.defaults,
|
|
1088
|
+
contextMenu: el.hasAttribute("data-context-menu"),
|
|
1037
1089
|
...options
|
|
1038
1090
|
};
|
|
1039
1091
|
this.isOpen = false;
|
|
@@ -1041,10 +1093,12 @@ var _Menu = class _Menu extends Component {
|
|
|
1041
1093
|
this.isTouchMoving = false;
|
|
1042
1094
|
this.focusedIndex = -1;
|
|
1043
1095
|
this.filterQuery = [];
|
|
1044
|
-
this.
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
this.el.
|
|
1096
|
+
if (!this.options.contextMenu) {
|
|
1097
|
+
this.el.ariaExpanded = "false";
|
|
1098
|
+
if (!this.el.hasAttribute("aria-haspopup")) this.el.setAttribute("aria-haspopup", "menu");
|
|
1099
|
+
if (this.id && !this.el.hasAttribute("aria-controls"))
|
|
1100
|
+
this.el.setAttribute("aria-controls", this.id);
|
|
1101
|
+
}
|
|
1048
1102
|
this._moveMenuToElement();
|
|
1049
1103
|
this._setupAccessibility();
|
|
1050
1104
|
this._makeMenuFocusable();
|
|
@@ -1068,7 +1122,7 @@ var _Menu = class _Menu extends Component {
|
|
|
1068
1122
|
destroy() {
|
|
1069
1123
|
this._clearPendingTimers();
|
|
1070
1124
|
this.isOpen = false;
|
|
1071
|
-
this.
|
|
1125
|
+
this._setExpanded(false);
|
|
1072
1126
|
clearTimeout(this.filterTimeout);
|
|
1073
1127
|
this._resetMenuStyles();
|
|
1074
1128
|
this._restoreInert();
|
|
@@ -1084,7 +1138,9 @@ var _Menu = class _Menu extends Component {
|
|
|
1084
1138
|
if (this.menuEl?.querySelector(":scope > li > menu")) {
|
|
1085
1139
|
this.menuEl.addEventListener("mouseover", this._handleSubmenuAlign);
|
|
1086
1140
|
}
|
|
1087
|
-
if (this.options.
|
|
1141
|
+
if (this.options.contextMenu) {
|
|
1142
|
+
this.el.addEventListener("contextmenu", this._handleContextMenu);
|
|
1143
|
+
} else if (this.options.hover) {
|
|
1088
1144
|
this.el.addEventListener("mouseenter", this._handleMouseEnter);
|
|
1089
1145
|
this.el.addEventListener("mouseleave", this._handleMouseLeave);
|
|
1090
1146
|
this.menuEl.addEventListener("mouseleave", this._handleMouseLeave);
|
|
@@ -1096,7 +1152,9 @@ var _Menu = class _Menu extends Component {
|
|
|
1096
1152
|
this.el.removeEventListener("keydown", this._handleTriggerKeydown);
|
|
1097
1153
|
this.menuEl?.removeEventListener("click", this._handleMenuClick);
|
|
1098
1154
|
this.menuEl?.removeEventListener("mouseover", this._handleSubmenuAlign);
|
|
1099
|
-
if (this.options.
|
|
1155
|
+
if (this.options.contextMenu) {
|
|
1156
|
+
this.el.removeEventListener("contextmenu", this._handleContextMenu);
|
|
1157
|
+
} else if (this.options.hover) {
|
|
1100
1158
|
this.el.removeEventListener("mouseenter", this._handleMouseEnter);
|
|
1101
1159
|
this.el.removeEventListener("mouseleave", this._handleMouseLeave);
|
|
1102
1160
|
this.menuEl?.removeEventListener("mouseleave", this._handleMouseLeave);
|
|
@@ -1112,6 +1170,7 @@ var _Menu = class _Menu extends Component {
|
|
|
1112
1170
|
});
|
|
1113
1171
|
this.menuEl?.addEventListener("keydown", this._handleMenuKeydown);
|
|
1114
1172
|
window.addEventListener("resize", this._handleWindowResize);
|
|
1173
|
+
if (this.options.contextMenu) document.addEventListener("contextmenu", this._handleDocumentContextMenu);
|
|
1115
1174
|
}
|
|
1116
1175
|
_removeTemporaryEventHandlers() {
|
|
1117
1176
|
document.body.removeEventListener("click", this._handleDocumentClick);
|
|
@@ -1119,6 +1178,7 @@ var _Menu = class _Menu extends Component {
|
|
|
1119
1178
|
document.body.removeEventListener("touchmove", this._handleDocumentTouchmove);
|
|
1120
1179
|
this.menuEl?.removeEventListener("keydown", this._handleMenuKeydown);
|
|
1121
1180
|
window.removeEventListener("resize", this._handleWindowResize);
|
|
1181
|
+
document.removeEventListener("contextmenu", this._handleDocumentContextMenu);
|
|
1122
1182
|
}
|
|
1123
1183
|
_setupAccessibility() {
|
|
1124
1184
|
if (!this.menuEl) return;
|
|
@@ -1408,8 +1468,12 @@ var _Menu = class _Menu extends Component {
|
|
|
1408
1468
|
closestOverflowParent.style.position = "relative";
|
|
1409
1469
|
const natural = this.menuEl.getBoundingClientRect().width;
|
|
1410
1470
|
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));
|
|
1471
|
+
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
1472
|
this.menuEl.style.width = idealWidth + "px";
|
|
1473
|
+
if (this.options.contextMenu) {
|
|
1474
|
+
this._placeMenuAtPoint(this._contextPoint ?? this._regionCorner(), closestOverflowParent);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1413
1477
|
const positionInfo = this._getMenuPosition(closestOverflowParent);
|
|
1414
1478
|
this.menuEl.style.left = positionInfo.x + "px";
|
|
1415
1479
|
this.menuEl.style.top = positionInfo.y + "px";
|
|
@@ -1417,6 +1481,58 @@ var _Menu = class _Menu extends Component {
|
|
|
1417
1481
|
this.menuEl.style.width = positionInfo.width + "px";
|
|
1418
1482
|
this.menuEl.style.transformOrigin = `${positionInfo.horizontalAlignment === "left" ? "0" : "100%"} ${positionInfo.verticalAlignment === "top" ? "0" : "100%"}`;
|
|
1419
1483
|
}
|
|
1484
|
+
/** Where a context menu opens without a pointer: the region's start corner. */
|
|
1485
|
+
_regionCorner() {
|
|
1486
|
+
const rect = this.el.getBoundingClientRect();
|
|
1487
|
+
const rtl = getComputedStyle(this.el).direction === "rtl";
|
|
1488
|
+
return { x: rtl ? rect.right : rect.left, y: rect.bottom };
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Places a context menu with its start corner on the point, flipping to
|
|
1492
|
+
* the other side of the point where it would not fit. The bounds are the
|
|
1493
|
+
* viewport, narrowed to a scrolling or clipping ancestor the menu sits in;
|
|
1494
|
+
* a menu taller than them gets their height and scrolls.
|
|
1495
|
+
*
|
|
1496
|
+
* The menu rests at left/top 0 of its containing block, so its rect is the
|
|
1497
|
+
* offset to subtract - the same trick _getMenuPosition uses. It is measured
|
|
1498
|
+
* without the entry animation's scale, which a second right-click can catch.
|
|
1499
|
+
*/
|
|
1500
|
+
_placeMenuAtPoint({ x, y }, container) {
|
|
1501
|
+
const { transform, transition } = this.menuEl.style;
|
|
1502
|
+
this.menuEl.style.transition = "none";
|
|
1503
|
+
this.menuEl.style.transform = "none";
|
|
1504
|
+
const menu = this.menuEl.getBoundingClientRect();
|
|
1505
|
+
this.menuEl.style.transform = transform;
|
|
1506
|
+
this.menuEl.style.transition = transition;
|
|
1507
|
+
let bounds = { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight };
|
|
1508
|
+
if (container && getComputedStyle(container).overflow !== "visible") {
|
|
1509
|
+
const clip = container.getBoundingClientRect();
|
|
1510
|
+
bounds = {
|
|
1511
|
+
left: Math.max(bounds.left, clip.left),
|
|
1512
|
+
top: Math.max(bounds.top, clip.top),
|
|
1513
|
+
right: Math.min(bounds.right, clip.right),
|
|
1514
|
+
bottom: Math.min(bounds.bottom, clip.bottom)
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
const width = menu.width;
|
|
1518
|
+
const height = Math.min(menu.height, bounds.bottom - bounds.top);
|
|
1519
|
+
this.isScrollable = height < menu.height;
|
|
1520
|
+
if (this.isScrollable) this.menuEl.style.height = height + "px";
|
|
1521
|
+
const rtl = getComputedStyle(this.el).direction === "rtl";
|
|
1522
|
+
const fitsRight = x + width <= bounds.right;
|
|
1523
|
+
const fitsLeft = x - width >= bounds.left;
|
|
1524
|
+
const leftward = rtl ? fitsLeft || !fitsRight : !fitsRight && fitsLeft;
|
|
1525
|
+
const upward = y + height > bounds.bottom && y - height >= bounds.top;
|
|
1526
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
|
|
1527
|
+
const left = clamp(leftward ? x - width : x, bounds.left, bounds.right - width);
|
|
1528
|
+
const top = clamp(upward ? y - height : y, bounds.top, bounds.bottom - height);
|
|
1529
|
+
this.menuEl.style.left = left - menu.left + "px";
|
|
1530
|
+
this.menuEl.style.top = top - menu.top + "px";
|
|
1531
|
+
this.menuEl.style.transformOrigin = `${leftward ? "100%" : "0"} ${upward ? "100%" : "0"}`;
|
|
1532
|
+
}
|
|
1533
|
+
_setExpanded(expanded) {
|
|
1534
|
+
if (!this.options.contextMenu) this.el.ariaExpanded = String(expanded);
|
|
1535
|
+
}
|
|
1420
1536
|
};
|
|
1421
1537
|
_Menu._menus = [];
|
|
1422
1538
|
var Menu = _Menu;
|
|
@@ -2731,6 +2847,10 @@ var Forms = class _Forms {
|
|
|
2731
2847
|
Utils.onDocumentReady(() => {
|
|
2732
2848
|
document.addEventListener("change", (e) => {
|
|
2733
2849
|
const target = e.target;
|
|
2850
|
+
if (target instanceof HTMLInputElement && target.type === "file") {
|
|
2851
|
+
const zone = target.closest(".drop-zone");
|
|
2852
|
+
if (zone) _Forms.listDropZoneFiles(zone, target);
|
|
2853
|
+
}
|
|
2734
2854
|
if (target instanceof HTMLInputElement) {
|
|
2735
2855
|
_Forms.validateField(target);
|
|
2736
2856
|
}
|
|
@@ -2746,6 +2866,12 @@ var Forms = class _Forms {
|
|
|
2746
2866
|
}
|
|
2747
2867
|
}
|
|
2748
2868
|
});
|
|
2869
|
+
document.addEventListener("click", _Forms.togglePassword);
|
|
2870
|
+
document.addEventListener("submit", _Forms.resetFormExtras, true);
|
|
2871
|
+
document.addEventListener("reset", _Forms.resetFormExtras, true);
|
|
2872
|
+
for (const type of ["dragenter", "dragover", "dragleave", "drop"]) {
|
|
2873
|
+
document.addEventListener(type, _Forms.markDropZone);
|
|
2874
|
+
}
|
|
2749
2875
|
document.querySelectorAll(".expressive-textarea").forEach((textArea) => {
|
|
2750
2876
|
_Forms.InitTextarea(textArea);
|
|
2751
2877
|
});
|
|
@@ -2754,6 +2880,75 @@ var Forms = class _Forms {
|
|
|
2754
2880
|
});
|
|
2755
2881
|
});
|
|
2756
2882
|
}
|
|
2883
|
+
/**
|
|
2884
|
+
* A .password-toggle button shows or hides the password in its field. The
|
|
2885
|
+
* button's own name stays put ("Show password"); aria-pressed carries the
|
|
2886
|
+
* state, so there is no generated text to translate.
|
|
2887
|
+
*/
|
|
2888
|
+
static togglePassword(e) {
|
|
2889
|
+
const button = e.target.closest?.(".password-toggle");
|
|
2890
|
+
const input = button?.closest(".field")?.querySelector(
|
|
2891
|
+
'input[type="password"], input[data-password]'
|
|
2892
|
+
);
|
|
2893
|
+
if (!input) return;
|
|
2894
|
+
const show = input.type === "password";
|
|
2895
|
+
input.toggleAttribute("data-password", show);
|
|
2896
|
+
input.type = show ? "text" : "password";
|
|
2897
|
+
button.setAttribute("aria-pressed", String(show));
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* A revealed password goes back to type="password" when its form is
|
|
2901
|
+
* submitted or reset, so it is never sent or autofilled as plain text and
|
|
2902
|
+
* a reset field does not stay revealed. A reset form also drops the file
|
|
2903
|
+
* lists it would otherwise leave showing: reset fires no change event.
|
|
2904
|
+
*/
|
|
2905
|
+
static resetFormExtras(e) {
|
|
2906
|
+
const form = e.target;
|
|
2907
|
+
if (!(form instanceof HTMLFormElement)) return;
|
|
2908
|
+
form.querySelectorAll("input[data-password]").forEach((input) => {
|
|
2909
|
+
input.removeAttribute("data-password");
|
|
2910
|
+
input.type = "password";
|
|
2911
|
+
input.closest(".field")?.querySelector(".password-toggle")?.setAttribute("aria-pressed", "false");
|
|
2912
|
+
});
|
|
2913
|
+
if (e.type === "reset") {
|
|
2914
|
+
form.querySelectorAll(".drop-zone-files").forEach((list) => list.replaceChildren());
|
|
2915
|
+
form.querySelectorAll(".drop-zone.dragover").forEach((zone) => zone.classList.remove("dragover"));
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
/**
|
|
2919
|
+
* The native file input covers its .drop-zone, so the browser already takes
|
|
2920
|
+
* a dropped file. This only shows the drag: the input is the topmost
|
|
2921
|
+
* element, so every drag event over the zone targets it and leaving it is
|
|
2922
|
+
* leaving the zone.
|
|
2923
|
+
*/
|
|
2924
|
+
static markDropZone(e) {
|
|
2925
|
+
const input = e.target;
|
|
2926
|
+
if (!(input instanceof HTMLInputElement) || input.type !== "file") return;
|
|
2927
|
+
const zone = input.closest(".drop-zone");
|
|
2928
|
+
if (!zone) return;
|
|
2929
|
+
const over = e.type === "dragenter" || e.type === "dragover";
|
|
2930
|
+
zone.classList.toggle("dragover", over && !input.disabled);
|
|
2931
|
+
}
|
|
2932
|
+
/** Lists the chosen files, name and size, in the zone's .drop-zone-files. */
|
|
2933
|
+
static listDropZoneFiles(zone, input) {
|
|
2934
|
+
const list = zone.querySelector(".drop-zone-files");
|
|
2935
|
+
if (!list) return;
|
|
2936
|
+
const size = (bytes) => {
|
|
2937
|
+
const [value, unit] = bytes >= 1e6 ? [bytes / 1e6, "megabyte"] : bytes >= 1e3 ? [bytes / 1e3, "kilobyte"] : [bytes, "byte"];
|
|
2938
|
+
return new Intl.NumberFormat(void 0, { style: "unit", unit, maximumFractionDigits: 1 }).format(value);
|
|
2939
|
+
};
|
|
2940
|
+
list.replaceChildren(
|
|
2941
|
+
...Array.from(input.files ?? [], (file) => {
|
|
2942
|
+
const item = document.createElement("li");
|
|
2943
|
+
const name = document.createElement("span");
|
|
2944
|
+
const meta = document.createElement("span");
|
|
2945
|
+
name.textContent = file.name;
|
|
2946
|
+
meta.textContent = size(file.size);
|
|
2947
|
+
item.append(name, meta);
|
|
2948
|
+
return item;
|
|
2949
|
+
})
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2757
2952
|
static InitTextarea(textarea) {
|
|
2758
2953
|
textarea.setAttribute("original-height", textarea.getBoundingClientRect().height.toString());
|
|
2759
2954
|
_Forms.textareaAutoResize(textarea);
|
|
@@ -3917,8 +4112,1576 @@ var CharacterCounter = class _CharacterCounter extends Component {
|
|
|
3917
4112
|
}
|
|
3918
4113
|
};
|
|
3919
4114
|
|
|
3920
|
-
// src/ts/components/
|
|
4115
|
+
// src/ts/components/commandPalette.ts
|
|
3921
4116
|
var _defaults12 = {
|
|
4117
|
+
shortcut: "k",
|
|
4118
|
+
closeOnRun: true
|
|
4119
|
+
};
|
|
4120
|
+
var APPLE = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
4121
|
+
var fold = (text) => text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
|
|
4122
|
+
var CommandPalette = class _CommandPalette extends Component {
|
|
4123
|
+
constructor(el, options) {
|
|
4124
|
+
super(el, options, _CommandPalette);
|
|
4125
|
+
this._active = null;
|
|
4126
|
+
this._generatedIds = [];
|
|
4127
|
+
/** Wires and refilters commands the page adds or removes later. */
|
|
4128
|
+
this._observer = null;
|
|
4129
|
+
/** Opens the palette as a modal dialog with the search input focused. */
|
|
4130
|
+
this.open = () => {
|
|
4131
|
+
if (!this.el.open) this.el.showModal();
|
|
4132
|
+
this.input?.focus();
|
|
4133
|
+
};
|
|
4134
|
+
/** Closes the palette. The dialog returns focus to where it was. */
|
|
4135
|
+
this.close = () => {
|
|
4136
|
+
if (this.el.open) this.el.close();
|
|
4137
|
+
};
|
|
4138
|
+
this._handleInput = () => this._filter();
|
|
4139
|
+
this._handleKeydown = (e) => {
|
|
4140
|
+
if (e.key === Utils.keys.ARROW_DOWN || e.key === Utils.keys.ARROW_UP) {
|
|
4141
|
+
e.preventDefault();
|
|
4142
|
+
this._move(e.key === Utils.keys.ARROW_DOWN ? 1 : -1);
|
|
4143
|
+
} else if (e.key === Utils.keys.ENTER && !e.isComposing && e.keyCode !== 229 && this._active) {
|
|
4144
|
+
e.preventDefault();
|
|
4145
|
+
this._run(this._active);
|
|
4146
|
+
}
|
|
4147
|
+
};
|
|
4148
|
+
this._handleListClick = (e) => {
|
|
4149
|
+
const item = e.target.closest('[role="option"]');
|
|
4150
|
+
if (!item || !this.list.contains(item) || item.getAttribute("aria-disabled") === "true") return;
|
|
4151
|
+
const action = this._action(item);
|
|
4152
|
+
if (action && !action.contains(e.target)) {
|
|
4153
|
+
action.click();
|
|
4154
|
+
return;
|
|
4155
|
+
}
|
|
4156
|
+
if (this.options.closeOnRun) this.close();
|
|
4157
|
+
};
|
|
4158
|
+
this._handlePointerMove = (e) => {
|
|
4159
|
+
const item = e.target.closest('[role="option"]');
|
|
4160
|
+
if (item && this.list.contains(item) && item.getAttribute("aria-disabled") !== "true") this._setActive(item);
|
|
4161
|
+
};
|
|
4162
|
+
/**
|
|
4163
|
+
* Each opening starts from the full list. The close event is queued, so a
|
|
4164
|
+
* palette reopened straight away may already be open again; its new search
|
|
4165
|
+
* is left alone.
|
|
4166
|
+
*/
|
|
4167
|
+
this._handleClose = () => {
|
|
4168
|
+
if (this.el.open) return;
|
|
4169
|
+
this.input.value = "";
|
|
4170
|
+
this._filter();
|
|
4171
|
+
};
|
|
4172
|
+
this._handleShortcut = (e) => {
|
|
4173
|
+
const { shortcut } = this.options;
|
|
4174
|
+
if (!shortcut || e.defaultPrevented || e.altKey || e.shiftKey) return;
|
|
4175
|
+
if (!(APPLE ? e.metaKey && !e.ctrlKey : e.ctrlKey && !e.metaKey)) return;
|
|
4176
|
+
const letter = shortcut.toLowerCase();
|
|
4177
|
+
const latin = /^[a-z]$/i.test(e.key);
|
|
4178
|
+
const hit = e.key.toLowerCase() === letter || !latin && /^[a-z]$/.test(letter) && e.code === `Key${letter.toUpperCase()}`;
|
|
4179
|
+
if (!hit) return;
|
|
4180
|
+
e.preventDefault();
|
|
4181
|
+
if (this.el.open) this.close();
|
|
4182
|
+
else this.open();
|
|
4183
|
+
};
|
|
4184
|
+
this.el["Expressive_CommandPalette"] = this;
|
|
4185
|
+
const shortcut = el.dataset.shortcut;
|
|
4186
|
+
this.options = {
|
|
4187
|
+
..._CommandPalette.defaults,
|
|
4188
|
+
...shortcut !== void 0 ? { shortcut: shortcut || null } : {},
|
|
4189
|
+
...options
|
|
4190
|
+
};
|
|
4191
|
+
this.input = el.querySelector(":scope > input");
|
|
4192
|
+
this.list = el.querySelector(":scope > :is(ul, ol)");
|
|
4193
|
+
if (!this.input || !this.list) {
|
|
4194
|
+
console.error("CommandPalette needs an <input> and a <ul> as children of the dialog.");
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
4197
|
+
this._setupAccessibility();
|
|
4198
|
+
this._filter();
|
|
4199
|
+
this._observer = new MutationObserver(() => this._filter(true));
|
|
4200
|
+
this._observer.observe(this.list, {
|
|
4201
|
+
childList: true,
|
|
4202
|
+
subtree: true,
|
|
4203
|
+
attributes: true,
|
|
4204
|
+
attributeFilter: ["hidden", "disabled"]
|
|
4205
|
+
});
|
|
4206
|
+
this.input.addEventListener("input", this._handleInput);
|
|
4207
|
+
this.input.addEventListener("keydown", this._handleKeydown);
|
|
4208
|
+
this.list.addEventListener("click", this._handleListClick);
|
|
4209
|
+
this.list.addEventListener("pointermove", this._handlePointerMove);
|
|
4210
|
+
this.el.addEventListener("close", this._handleClose);
|
|
4211
|
+
document.addEventListener("keydown", this._handleShortcut);
|
|
4212
|
+
}
|
|
4213
|
+
static get defaults() {
|
|
4214
|
+
return _defaults12;
|
|
4215
|
+
}
|
|
4216
|
+
/**
|
|
4217
|
+
* Initializes instances of CommandPalette.
|
|
4218
|
+
* @param els HTML elements.
|
|
4219
|
+
* @param options Component options.
|
|
4220
|
+
*/
|
|
4221
|
+
static init(els, options = {}) {
|
|
4222
|
+
return super.init(els, options, _CommandPalette);
|
|
4223
|
+
}
|
|
4224
|
+
static getInstance(el) {
|
|
4225
|
+
return el["Expressive_CommandPalette"];
|
|
4226
|
+
}
|
|
4227
|
+
/** If the palette is open. */
|
|
4228
|
+
get isOpen() {
|
|
4229
|
+
return this.el.open;
|
|
4230
|
+
}
|
|
4231
|
+
destroy() {
|
|
4232
|
+
this._observer?.disconnect();
|
|
4233
|
+
document.removeEventListener("keydown", this._handleShortcut);
|
|
4234
|
+
this.el.removeEventListener("close", this._handleClose);
|
|
4235
|
+
if (this.input && this.list) {
|
|
4236
|
+
this.input.removeEventListener("input", this._handleInput);
|
|
4237
|
+
this.input.removeEventListener("keydown", this._handleKeydown);
|
|
4238
|
+
this.list.removeEventListener("click", this._handleListClick);
|
|
4239
|
+
this.list.removeEventListener("pointermove", this._handlePointerMove);
|
|
4240
|
+
for (const name of ["role", "aria-controls", "aria-expanded", "aria-autocomplete", "aria-activedescendant"]) {
|
|
4241
|
+
this.input.removeAttribute(name);
|
|
4242
|
+
}
|
|
4243
|
+
this.list.removeAttribute("role");
|
|
4244
|
+
for (const item of this._items()) {
|
|
4245
|
+
for (const name of ["role", "aria-selected", "aria-disabled", "data-filtered"]) item.removeAttribute(name);
|
|
4246
|
+
item.classList.remove("active");
|
|
4247
|
+
this._action(item)?.removeAttribute("tabindex");
|
|
4248
|
+
}
|
|
4249
|
+
for (const label of this._labels()) {
|
|
4250
|
+
label.removeAttribute("role");
|
|
4251
|
+
label.removeAttribute("data-filtered");
|
|
4252
|
+
}
|
|
4253
|
+
for (const el of this._generatedIds) el.removeAttribute("id");
|
|
4254
|
+
const empty = this._empty();
|
|
4255
|
+
if (empty) empty.hidden = false;
|
|
4256
|
+
}
|
|
4257
|
+
this.el["Expressive_CommandPalette"] = void 0;
|
|
4258
|
+
}
|
|
4259
|
+
_items() {
|
|
4260
|
+
return Array.from(this.list.children).filter(
|
|
4261
|
+
(child) => child instanceof HTMLElement && !child.classList.contains("label")
|
|
4262
|
+
);
|
|
4263
|
+
}
|
|
4264
|
+
_labels() {
|
|
4265
|
+
return Array.from(this.list.querySelectorAll(":scope > .label"));
|
|
4266
|
+
}
|
|
4267
|
+
_empty() {
|
|
4268
|
+
return this.el.querySelector(":scope > .command-palette-empty");
|
|
4269
|
+
}
|
|
4270
|
+
_action(item) {
|
|
4271
|
+
return item.querySelector(":scope > :is(a[href], button)");
|
|
4272
|
+
}
|
|
4273
|
+
_ensureId(el, prefix) {
|
|
4274
|
+
if (!el.id) {
|
|
4275
|
+
el.id = `${prefix}-${Utils.guid()}`;
|
|
4276
|
+
this._generatedIds.push(el);
|
|
4277
|
+
}
|
|
4278
|
+
return el.id;
|
|
4279
|
+
}
|
|
4280
|
+
_setupAccessibility() {
|
|
4281
|
+
this.list.setAttribute("role", "listbox");
|
|
4282
|
+
this.input.setAttribute("role", "combobox");
|
|
4283
|
+
this.input.setAttribute("aria-controls", this._ensureId(this.list, "command-palette-list"));
|
|
4284
|
+
this.input.setAttribute("aria-expanded", "true");
|
|
4285
|
+
this.input.setAttribute("aria-autocomplete", "list");
|
|
4286
|
+
}
|
|
4287
|
+
/** Every command, including ones added since the last pass, is an option. */
|
|
4288
|
+
_wireItems() {
|
|
4289
|
+
for (const item of this._items()) {
|
|
4290
|
+
if (item.getAttribute("role") === "option") continue;
|
|
4291
|
+
item.setAttribute("role", "option");
|
|
4292
|
+
this._ensureId(item, "command-palette-option");
|
|
4293
|
+
this._action(item)?.setAttribute("tabindex", "-1");
|
|
4294
|
+
}
|
|
4295
|
+
for (const label of this._labels()) label.setAttribute("role", "none");
|
|
4296
|
+
}
|
|
4297
|
+
/** Commands on screen: neither hidden by the page nor filtered out. */
|
|
4298
|
+
_shown() {
|
|
4299
|
+
return this._items().filter((item) => !item.hidden && !item.hasAttribute("data-filtered"));
|
|
4300
|
+
}
|
|
4301
|
+
/** Shown commands the arrows and Enter can reach: disabled ones are skipped. */
|
|
4302
|
+
_visible() {
|
|
4303
|
+
return this._shown().filter((item) => item.getAttribute("aria-disabled") !== "true");
|
|
4304
|
+
}
|
|
4305
|
+
/** A disabled link or button cannot run, so its option says so. */
|
|
4306
|
+
_isDisabled(item) {
|
|
4307
|
+
const action = this._action(item);
|
|
4308
|
+
return !!action && (action.matches(":disabled") || action.getAttribute("aria-disabled") === "true");
|
|
4309
|
+
}
|
|
4310
|
+
_setActive(item) {
|
|
4311
|
+
if (this._active === item) return;
|
|
4312
|
+
this._active?.classList.remove("active");
|
|
4313
|
+
this._active?.removeAttribute("aria-selected");
|
|
4314
|
+
this._active = item;
|
|
4315
|
+
if (item) {
|
|
4316
|
+
item.classList.add("active");
|
|
4317
|
+
item.setAttribute("aria-selected", "true");
|
|
4318
|
+
this.input.setAttribute("aria-activedescendant", item.id);
|
|
4319
|
+
item.scrollIntoView({ block: "nearest" });
|
|
4320
|
+
} else {
|
|
4321
|
+
this.input.removeAttribute("aria-activedescendant");
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
/**
|
|
4325
|
+
* Shows the commands matching the query. Filtering marks rows with
|
|
4326
|
+
* data-filtered rather than hidden, so a command the page hides stays
|
|
4327
|
+
* hidden. `keepActive` holds the active command while it is still
|
|
4328
|
+
* reachable, for list changes the user did not make.
|
|
4329
|
+
*/
|
|
4330
|
+
_filter(keepActive = false) {
|
|
4331
|
+
this._wireItems();
|
|
4332
|
+
const terms = fold(this.input.value).split(/\s+/).filter(Boolean);
|
|
4333
|
+
for (const item of this._items()) {
|
|
4334
|
+
const keywords = item.dataset.keywords ?? this._action(item)?.dataset.keywords ?? "";
|
|
4335
|
+
const text = fold(`${item.textContent} ${keywords}`);
|
|
4336
|
+
item.toggleAttribute("data-filtered", !terms.every((term) => text.includes(term)));
|
|
4337
|
+
if (this._isDisabled(item)) item.setAttribute("aria-disabled", "true");
|
|
4338
|
+
else item.removeAttribute("aria-disabled");
|
|
4339
|
+
}
|
|
4340
|
+
for (const label of this._labels()) {
|
|
4341
|
+
let next = label.nextElementSibling;
|
|
4342
|
+
let any = false;
|
|
4343
|
+
while (next && !next.classList.contains("label")) {
|
|
4344
|
+
if (!next.hidden && !next.hasAttribute("data-filtered")) any = true;
|
|
4345
|
+
next = next.nextElementSibling;
|
|
4346
|
+
}
|
|
4347
|
+
label.toggleAttribute("data-filtered", !any);
|
|
4348
|
+
}
|
|
4349
|
+
const visible = this._visible();
|
|
4350
|
+
const empty = this._empty();
|
|
4351
|
+
if (empty) empty.hidden = this._shown().length > 0;
|
|
4352
|
+
this._setActive(keepActive && visible.includes(this._active) ? this._active : visible[0] ?? null);
|
|
4353
|
+
}
|
|
4354
|
+
_move(step) {
|
|
4355
|
+
const visible = this._visible();
|
|
4356
|
+
if (!visible.length) return;
|
|
4357
|
+
const index = visible.indexOf(this._active);
|
|
4358
|
+
this._setActive(visible[(index + step + visible.length) % visible.length]);
|
|
4359
|
+
}
|
|
4360
|
+
/** Runs a command by clicking its link or button; the click handler closes. */
|
|
4361
|
+
_run(item) {
|
|
4362
|
+
(this._action(item) ?? item).click();
|
|
4363
|
+
}
|
|
4364
|
+
};
|
|
4365
|
+
|
|
4366
|
+
// src/ts/components/questionnaire.ts
|
|
4367
|
+
var _defaults13 = {
|
|
4368
|
+
progressLabel: "Question {current} of {total}",
|
|
4369
|
+
requiredMessage: "Choose an answer to continue."
|
|
4370
|
+
};
|
|
4371
|
+
var NAVIGATION = ["previous", "skip", "next"];
|
|
4372
|
+
var Questionnaire = class _Questionnaire extends Component {
|
|
4373
|
+
constructor(el, options) {
|
|
4374
|
+
super(el, options, _Questionnaire);
|
|
4375
|
+
this._active = null;
|
|
4376
|
+
this._generated = [];
|
|
4377
|
+
this._generatedIds = [];
|
|
4378
|
+
/** Set while one submission's invalid events arrive, so only the first opens. */
|
|
4379
|
+
this._reporting = false;
|
|
4380
|
+
/** Shows the question at `index` without checking the current one. */
|
|
4381
|
+
this.goTo = (index) => {
|
|
4382
|
+
const item = this.items[index];
|
|
4383
|
+
if (!item) return;
|
|
4384
|
+
this._active = item;
|
|
4385
|
+
this._render();
|
|
4386
|
+
const controls = this._controls(item).filter((control) => !control.disabled);
|
|
4387
|
+
(controls.find((control) => control.checked) ?? controls[0])?.focus();
|
|
4388
|
+
};
|
|
4389
|
+
/** Checks the shown question and moves to the next one if it is answered. */
|
|
4390
|
+
this.next = () => {
|
|
4391
|
+
if (this._validate(this._active)) this.goTo(this.index + 1);
|
|
4392
|
+
};
|
|
4393
|
+
/** Moves to the previous question. */
|
|
4394
|
+
this.previous = () => this.goTo(this.index - 1);
|
|
4395
|
+
/** Clears the answer to the shown question and moves to the next one. */
|
|
4396
|
+
this.skip = () => {
|
|
4397
|
+
if (!this._active) return;
|
|
4398
|
+
for (const control of this._controls(this._active)) {
|
|
4399
|
+
const isChoice = control instanceof HTMLInputElement && (control.type === "radio" || control.type === "checkbox");
|
|
4400
|
+
if (isChoice ? !control.checked : !control.value) continue;
|
|
4401
|
+
if (isChoice) control.checked = false;
|
|
4402
|
+
else control.value = "";
|
|
4403
|
+
control.dispatchEvent(new Event("change", { bubbles: true }));
|
|
4404
|
+
}
|
|
4405
|
+
this.goTo(this.index + 1);
|
|
4406
|
+
};
|
|
4407
|
+
this._handleClick = (e) => {
|
|
4408
|
+
const button = e.target.closest?.("button");
|
|
4409
|
+
if (!button || !this._buttons().includes(button)) return;
|
|
4410
|
+
if (NAVIGATION.includes(button.value)) {
|
|
4411
|
+
e.preventDefault();
|
|
4412
|
+
this[button.value]();
|
|
4413
|
+
return;
|
|
4414
|
+
}
|
|
4415
|
+
this._syncRequired();
|
|
4416
|
+
};
|
|
4417
|
+
// Enter in a field moves on instead of submitting the form early.
|
|
4418
|
+
this._handleKeydown = (e) => {
|
|
4419
|
+
if (e.key !== "Enter" || e.defaultPrevented || e.isComposing) return;
|
|
4420
|
+
if (!(e.target instanceof HTMLInputElement) || this.index >= this.items.length - 1) return;
|
|
4421
|
+
e.preventDefault();
|
|
4422
|
+
this.next();
|
|
4423
|
+
};
|
|
4424
|
+
// A submission found an unanswered question, possibly a hidden one. Open the
|
|
4425
|
+
// first instead of letting the browser report a control it cannot show.
|
|
4426
|
+
// One submission fires its invalid events in a single task.
|
|
4427
|
+
this._handleInvalid = (e) => {
|
|
4428
|
+
const item = this.items.find((item2) => item2.contains(e.target));
|
|
4429
|
+
if (!item) return;
|
|
4430
|
+
e.preventDefault();
|
|
4431
|
+
if (this._reporting) return;
|
|
4432
|
+
this._reporting = true;
|
|
4433
|
+
setTimeout(() => this._reporting = false);
|
|
4434
|
+
this._active = item;
|
|
4435
|
+
this._render();
|
|
4436
|
+
this._validate(item);
|
|
4437
|
+
};
|
|
4438
|
+
// An answer clears the question's error.
|
|
4439
|
+
this._handleInput = (e) => {
|
|
4440
|
+
this._syncRequired();
|
|
4441
|
+
const item = e.target.closest?.("fieldset");
|
|
4442
|
+
if (item instanceof HTMLFieldSetElement && item.classList.contains("invalid")) this._setInvalid(item, false);
|
|
4443
|
+
};
|
|
4444
|
+
this.el["Expressive_Questionnaire"] = this;
|
|
4445
|
+
const progressLabel = el.dataset.progressLabel;
|
|
4446
|
+
this.options = {
|
|
4447
|
+
..._Questionnaire.defaults,
|
|
4448
|
+
...progressLabel ? { progressLabel } : {},
|
|
4449
|
+
...options
|
|
4450
|
+
};
|
|
4451
|
+
this._progress = document.createElement("div");
|
|
4452
|
+
this._progress.className = "questionnaire-progress";
|
|
4453
|
+
this._progressText = document.createElement("span");
|
|
4454
|
+
this._progressText.id = `questionnaire-progress-${Utils.guid()}`;
|
|
4455
|
+
this._progressBar = document.createElement("progress");
|
|
4456
|
+
this._progressBar.className = "progress";
|
|
4457
|
+
this._progressBar.setAttribute("aria-labelledby", this._progressText.id);
|
|
4458
|
+
this._progress.append(this._progressText, this._progressBar);
|
|
4459
|
+
this.el.prepend(this._progress);
|
|
4460
|
+
for (const item of this._fieldsets()) {
|
|
4461
|
+
const ids = Array.from(item.querySelectorAll(":scope > p:not(.questionnaire-error)")).map((p) => this._ensureId(p, "questionnaire-description"));
|
|
4462
|
+
if (ids.length && !item.hasAttribute("aria-describedby")) {
|
|
4463
|
+
item.setAttribute("aria-describedby", ids.join(" "));
|
|
4464
|
+
this._generatedIds.push(item);
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
this._active = this.items[0] ?? null;
|
|
4468
|
+
this._syncRequired();
|
|
4469
|
+
this._render();
|
|
4470
|
+
this._observer = new MutationObserver(() => {
|
|
4471
|
+
this._syncRequired();
|
|
4472
|
+
this._render();
|
|
4473
|
+
});
|
|
4474
|
+
this._observer.observe(this.el, { attributes: true, subtree: true, attributeFilter: ["disabled"] });
|
|
4475
|
+
this.el.addEventListener("click", this._handleClick);
|
|
4476
|
+
this.el.addEventListener("keydown", this._handleKeydown);
|
|
4477
|
+
this.el.addEventListener("input", this._handleInput);
|
|
4478
|
+
this.el.addEventListener("invalid", this._handleInvalid, true);
|
|
4479
|
+
}
|
|
4480
|
+
static get defaults() {
|
|
4481
|
+
return _defaults13;
|
|
4482
|
+
}
|
|
4483
|
+
/**
|
|
4484
|
+
* Initializes instances of Questionnaire.
|
|
4485
|
+
* @param els HTML elements.
|
|
4486
|
+
* @param options Component options.
|
|
4487
|
+
*/
|
|
4488
|
+
static init(els, options = {}) {
|
|
4489
|
+
return super.init(els, options, _Questionnaire);
|
|
4490
|
+
}
|
|
4491
|
+
static getInstance(el) {
|
|
4492
|
+
return el["Expressive_Questionnaire"];
|
|
4493
|
+
}
|
|
4494
|
+
/** The questions that apply: every fieldset child that is not disabled. */
|
|
4495
|
+
get items() {
|
|
4496
|
+
return this._fieldsets().filter((item) => !item.disabled);
|
|
4497
|
+
}
|
|
4498
|
+
/** Position of the shown question in `items`. */
|
|
4499
|
+
get index() {
|
|
4500
|
+
return this.items.indexOf(this._active);
|
|
4501
|
+
}
|
|
4502
|
+
destroy() {
|
|
4503
|
+
this._observer.disconnect();
|
|
4504
|
+
this.el.removeEventListener("click", this._handleClick);
|
|
4505
|
+
this.el.removeEventListener("keydown", this._handleKeydown);
|
|
4506
|
+
this.el.removeEventListener("input", this._handleInput);
|
|
4507
|
+
this.el.removeEventListener("invalid", this._handleInvalid, true);
|
|
4508
|
+
for (const item of this._fieldsets()) {
|
|
4509
|
+
if (item.hasAttribute("data-required")) for (const control of this._controls(item)) control.setCustomValidity("");
|
|
4510
|
+
item.hidden = false;
|
|
4511
|
+
this._setInvalid(item, false);
|
|
4512
|
+
}
|
|
4513
|
+
for (const button of this._buttons()) button.hidden = false;
|
|
4514
|
+
this._progress.remove();
|
|
4515
|
+
for (const el of this._generated) el.remove();
|
|
4516
|
+
for (const el of this._generatedIds) {
|
|
4517
|
+
el.removeAttribute(el instanceof HTMLFieldSetElement ? "aria-describedby" : "id");
|
|
4518
|
+
}
|
|
4519
|
+
this.el["Expressive_Questionnaire"] = void 0;
|
|
4520
|
+
}
|
|
4521
|
+
_fieldsets() {
|
|
4522
|
+
return Array.from(this.el.querySelectorAll(":scope > fieldset"));
|
|
4523
|
+
}
|
|
4524
|
+
/** The footer's navigation and submit buttons. */
|
|
4525
|
+
_buttons() {
|
|
4526
|
+
return Array.from(this.el.querySelectorAll(":scope > footer > button")).filter(
|
|
4527
|
+
(button) => NAVIGATION.includes(button.value) || button.type === "submit"
|
|
4528
|
+
);
|
|
4529
|
+
}
|
|
4530
|
+
_controls(item) {
|
|
4531
|
+
return Array.from(item.elements).filter(
|
|
4532
|
+
(el) => el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement || el instanceof HTMLInputElement && el.type !== "button" && el.type !== "submit"
|
|
4533
|
+
);
|
|
4534
|
+
}
|
|
4535
|
+
_required(item) {
|
|
4536
|
+
return item.hasAttribute("data-required") || this._controls(item).some((control) => control.required);
|
|
4537
|
+
}
|
|
4538
|
+
_ensureId(el, prefix) {
|
|
4539
|
+
if (!el.id) {
|
|
4540
|
+
el.id = `${prefix}-${Utils.guid()}`;
|
|
4541
|
+
this._generatedIds.push(el);
|
|
4542
|
+
}
|
|
4543
|
+
return el.id;
|
|
4544
|
+
}
|
|
4545
|
+
_render() {
|
|
4546
|
+
const items = this.items;
|
|
4547
|
+
if (!items.includes(this._active)) {
|
|
4548
|
+
const all = this._fieldsets();
|
|
4549
|
+
const after = all.slice(all.indexOf(this._active) + 1).find((item) => !item.disabled);
|
|
4550
|
+
this._active = after ?? items[items.length - 1] ?? null;
|
|
4551
|
+
}
|
|
4552
|
+
const index = items.indexOf(this._active);
|
|
4553
|
+
const last = index === items.length - 1;
|
|
4554
|
+
for (const item of this._fieldsets()) item.hidden = item !== this._active;
|
|
4555
|
+
for (const button of this._buttons()) {
|
|
4556
|
+
if (button.value === "previous") button.hidden = index <= 0;
|
|
4557
|
+
else if (button.value === "skip") button.hidden = last || !this._active || this._required(this._active);
|
|
4558
|
+
else if (button.value === "next") button.hidden = last;
|
|
4559
|
+
else button.hidden = !last;
|
|
4560
|
+
}
|
|
4561
|
+
this._progressText.textContent = this.options.progressLabel.replace("{current}", String(index + 1)).replace("{total}", String(items.length));
|
|
4562
|
+
this._progressBar.max = Math.max(items.length, 1);
|
|
4563
|
+
this._progressBar.value = index + 1;
|
|
4564
|
+
}
|
|
4565
|
+
/**
|
|
4566
|
+
* Puts "choose at least one" on the first enabled checkbox of each
|
|
4567
|
+
* data-required group with none checked, so the browser's validation, and
|
|
4568
|
+
* every way of submitting, sees it. A disabled checkbox is not sent, so it
|
|
4569
|
+
* does not count.
|
|
4570
|
+
*/
|
|
4571
|
+
_syncRequired() {
|
|
4572
|
+
for (const item of this._fieldsets()) {
|
|
4573
|
+
if (!item.hasAttribute("data-required")) continue;
|
|
4574
|
+
const boxes = this._controls(item).filter(
|
|
4575
|
+
(control) => control instanceof HTMLInputElement && control.type === "checkbox"
|
|
4576
|
+
);
|
|
4577
|
+
const enabled = boxes.filter((box) => !box.disabled);
|
|
4578
|
+
for (const box of boxes) box.setCustomValidity("");
|
|
4579
|
+
if (enabled.length && !enabled.some((box) => box.checked)) {
|
|
4580
|
+
const error = item.querySelector(":scope > .questionnaire-error:not([data-generated])");
|
|
4581
|
+
enabled[0].setCustomValidity(error?.textContent.trim() || this.options.requiredMessage);
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
/** The first control in the question that still needs an answer. */
|
|
4586
|
+
_invalidControl(item) {
|
|
4587
|
+
return this._controls(item).find((control) => control.willValidate && !control.validity.valid);
|
|
4588
|
+
}
|
|
4589
|
+
/** Shows the question's error and returns false when it needs an answer. */
|
|
4590
|
+
_validate(item) {
|
|
4591
|
+
this._syncRequired();
|
|
4592
|
+
const invalid = item && this._invalidControl(item);
|
|
4593
|
+
if (!invalid) return true;
|
|
4594
|
+
let error = item.querySelector(":scope > .questionnaire-error");
|
|
4595
|
+
if (!error) {
|
|
4596
|
+
error = document.createElement("p");
|
|
4597
|
+
error.className = "questionnaire-error";
|
|
4598
|
+
error.dataset.generated = "";
|
|
4599
|
+
error.textContent = invalid.validationMessage || this.options.requiredMessage;
|
|
4600
|
+
item.append(error);
|
|
4601
|
+
this._generated.push(error);
|
|
4602
|
+
}
|
|
4603
|
+
this._setInvalid(item, true);
|
|
4604
|
+
invalid.focus();
|
|
4605
|
+
return false;
|
|
4606
|
+
}
|
|
4607
|
+
_setInvalid(item, invalid) {
|
|
4608
|
+
item.classList.toggle("invalid", invalid);
|
|
4609
|
+
const error = item.querySelector(":scope > .questionnaire-error");
|
|
4610
|
+
const described = (item.getAttribute("aria-describedby") ?? "").split(" ").filter((id) => id && id !== error?.id);
|
|
4611
|
+
if (invalid && error) described.push(this._ensureId(error, "questionnaire-error"));
|
|
4612
|
+
if (described.length) item.setAttribute("aria-describedby", described.join(" "));
|
|
4613
|
+
else item.removeAttribute("aria-describedby");
|
|
4614
|
+
for (const control of this._controls(item)) {
|
|
4615
|
+
if (invalid) control.setAttribute("aria-invalid", "true");
|
|
4616
|
+
else control.removeAttribute("aria-invalid");
|
|
4617
|
+
}
|
|
4618
|
+
if (!invalid && this._generated.includes(error)) {
|
|
4619
|
+
error.remove();
|
|
4620
|
+
this._generated.splice(this._generated.indexOf(error), 1);
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
};
|
|
4624
|
+
|
|
4625
|
+
// src/ts/components/lineChart.ts
|
|
4626
|
+
var _defaults14 = {
|
|
4627
|
+
min: null,
|
|
4628
|
+
max: null
|
|
4629
|
+
};
|
|
4630
|
+
var SVG = "http://www.w3.org/2000/svg";
|
|
4631
|
+
var COLORS = 4;
|
|
4632
|
+
function svgNode(name, attributes = {}) {
|
|
4633
|
+
const element = document.createElementNS(SVG, name);
|
|
4634
|
+
for (const key in attributes) element.setAttribute(key, attributes[key]);
|
|
4635
|
+
return element;
|
|
4636
|
+
}
|
|
4637
|
+
var _gradientId = 0;
|
|
4638
|
+
function cellValue(cell) {
|
|
4639
|
+
if (!cell) return null;
|
|
4640
|
+
const raw = cell.dataset.value ?? cell.textContent;
|
|
4641
|
+
const text = raw.replace(/\u2212/g, "-").replace(/[\s%\p{Sc}]/gu, "");
|
|
4642
|
+
if (!/^[+-]?(\d{1,3}(,\d{3})+|\d+)?(\.\d+)?(e[+-]?\d+)?$/i.test(text) || !/\d/.test(text)) return null;
|
|
4643
|
+
return parseFloat(text.replace(/,/g, ""));
|
|
4644
|
+
}
|
|
4645
|
+
function monotonePath(points) {
|
|
4646
|
+
const n = points.length;
|
|
4647
|
+
if (n === 0) return "";
|
|
4648
|
+
if (n === 1) return `M${points[0][0]},${points[0][1]}h0`;
|
|
4649
|
+
const slopes = [];
|
|
4650
|
+
for (let i = 0; i < n - 1; i++) {
|
|
4651
|
+
slopes.push((points[i + 1][1] - points[i][1]) / (points[i + 1][0] - points[i][0]));
|
|
4652
|
+
}
|
|
4653
|
+
const tangents = points.map((_, i) => {
|
|
4654
|
+
if (i === 0) return slopes[0];
|
|
4655
|
+
if (i === n - 1) return slopes[n - 2];
|
|
4656
|
+
const [a, b] = [slopes[i - 1], slopes[i]];
|
|
4657
|
+
return a * b <= 0 ? 0 : 2 / (1 / a + 1 / b);
|
|
4658
|
+
});
|
|
4659
|
+
let d = `M${points[0][0]},${points[0][1]}`;
|
|
4660
|
+
for (let i = 0; i < n - 1; i++) {
|
|
4661
|
+
const [[x0, y0], [x1, y1]] = [points[i], points[i + 1]];
|
|
4662
|
+
const h = (x1 - x0) / 3;
|
|
4663
|
+
d += `C${x0 + h},${y0 + tangents[i] * h} ${x1 - h},${y1 - tangents[i + 1] * h} ${x1},${y1}`;
|
|
4664
|
+
}
|
|
4665
|
+
return d;
|
|
4666
|
+
}
|
|
4667
|
+
var LineChart = class extends Component {
|
|
4668
|
+
constructor(el, options) {
|
|
4669
|
+
super(el, options, new.target);
|
|
4670
|
+
/** The values' x positions and series, read from the table. */
|
|
4671
|
+
this.labels = [];
|
|
4672
|
+
this.series = [];
|
|
4673
|
+
/** The highlighted row, or -1. */
|
|
4674
|
+
this.activeIndex = -1;
|
|
4675
|
+
this._generated = [];
|
|
4676
|
+
/** The table's header cells and body rows, as read. */
|
|
4677
|
+
this._header = [];
|
|
4678
|
+
this._rows = [];
|
|
4679
|
+
/**
|
|
4680
|
+
* Whether the rows run right to left: a horizontal row axis in a
|
|
4681
|
+
* right-to-left context, like the labels. Bar chart rows keep their order.
|
|
4682
|
+
*/
|
|
4683
|
+
this._reversed = false;
|
|
4684
|
+
this._onPointerMove = (e) => {
|
|
4685
|
+
this.show(this._indexAt(e));
|
|
4686
|
+
};
|
|
4687
|
+
this._onFocus = () => {
|
|
4688
|
+
if (this.activeIndex < 0 && this._plot.matches(":focus-visible")) this.show(0);
|
|
4689
|
+
};
|
|
4690
|
+
this._onLeave = (e) => {
|
|
4691
|
+
if (e.pointerType === "touch" || this._plot.matches(":focus-visible")) return;
|
|
4692
|
+
this.show(-1);
|
|
4693
|
+
};
|
|
4694
|
+
/** The browser took the touch to scroll the page. */
|
|
4695
|
+
this._onCancel = () => {
|
|
4696
|
+
this.show(-1);
|
|
4697
|
+
};
|
|
4698
|
+
this._onKeyDown = (e) => {
|
|
4699
|
+
const next = this._keyTarget(e.key);
|
|
4700
|
+
if (next === void 0) return;
|
|
4701
|
+
e.preventDefault();
|
|
4702
|
+
this.show(next);
|
|
4703
|
+
};
|
|
4704
|
+
this.el[this._key] = this;
|
|
4705
|
+
this.options = {
|
|
4706
|
+
...new.target.defaults,
|
|
4707
|
+
...options
|
|
4708
|
+
};
|
|
4709
|
+
this._read();
|
|
4710
|
+
if (!this.labels.length || !this.series.length) return;
|
|
4711
|
+
this._reversed = !this._horizontal && getComputedStyle(this.el).direction === "rtl";
|
|
4712
|
+
this._render();
|
|
4713
|
+
if (!this.el.classList.contains("sparkline")) {
|
|
4714
|
+
this._plot.tabIndex = 0;
|
|
4715
|
+
this._plot.addEventListener("pointerdown", this._onPointerMove);
|
|
4716
|
+
this._plot.addEventListener("pointermove", this._onPointerMove);
|
|
4717
|
+
this._plot.addEventListener("pointerleave", this._onLeave);
|
|
4718
|
+
this._plot.addEventListener("pointercancel", this._onCancel);
|
|
4719
|
+
this._plot.addEventListener("keydown", this._onKeyDown);
|
|
4720
|
+
this._plot.addEventListener("focus", this._onFocus);
|
|
4721
|
+
this._plot.addEventListener("blur", this._onLeave);
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
/** The class prefix of the generated parts; ColumnChart reuses this class. */
|
|
4725
|
+
get _type() {
|
|
4726
|
+
return "line-chart";
|
|
4727
|
+
}
|
|
4728
|
+
/** Whether the y axis always includes zero, as columns grow from it. */
|
|
4729
|
+
get _fromZero() {
|
|
4730
|
+
return false;
|
|
4731
|
+
}
|
|
4732
|
+
/**
|
|
4733
|
+
* Whether the rows run down the plot and the values across it, as in a
|
|
4734
|
+
* bar chart. The rows then keep their order in a right-to-left context.
|
|
4735
|
+
*/
|
|
4736
|
+
get _horizontal() {
|
|
4737
|
+
return false;
|
|
4738
|
+
}
|
|
4739
|
+
/**
|
|
4740
|
+
* Whether the rows go round a circle, as in a pie or radar chart. The
|
|
4741
|
+
* arrows then step round it, wrapping from the last row to the first.
|
|
4742
|
+
*/
|
|
4743
|
+
get _circular() {
|
|
4744
|
+
return false;
|
|
4745
|
+
}
|
|
4746
|
+
/** Whether the cursor marks each series with a point. */
|
|
4747
|
+
get _points() {
|
|
4748
|
+
return true;
|
|
4749
|
+
}
|
|
4750
|
+
get _key() {
|
|
4751
|
+
return "Expressive_LineChart";
|
|
4752
|
+
}
|
|
4753
|
+
static get defaults() {
|
|
4754
|
+
return _defaults14;
|
|
4755
|
+
}
|
|
4756
|
+
static init(els, options = {}) {
|
|
4757
|
+
return super.init(els, options, this);
|
|
4758
|
+
}
|
|
4759
|
+
static getInstance(el) {
|
|
4760
|
+
return el["Expressive_LineChart"];
|
|
4761
|
+
}
|
|
4762
|
+
destroy() {
|
|
4763
|
+
this._generated.forEach((node) => node.remove());
|
|
4764
|
+
this._generated = [];
|
|
4765
|
+
this.el[this._key] = void 0;
|
|
4766
|
+
}
|
|
4767
|
+
/**
|
|
4768
|
+
* Highlights one row: moves the cursor and fills the tooltip.
|
|
4769
|
+
* Pass -1 to hide them. Does nothing for a chart without data.
|
|
4770
|
+
*/
|
|
4771
|
+
show(index) {
|
|
4772
|
+
if (!this._tooltip || index === this.activeIndex) return;
|
|
4773
|
+
this.activeIndex = index;
|
|
4774
|
+
const hidden = index < 0 || index >= this.labels.length;
|
|
4775
|
+
this._cursor.hidden = this._tooltip.hidden = hidden;
|
|
4776
|
+
if (hidden) return;
|
|
4777
|
+
this._cursor.replaceChildren();
|
|
4778
|
+
const title = document.createElement("strong");
|
|
4779
|
+
title.textContent = this.labels[index];
|
|
4780
|
+
this._tooltip.replaceChildren(title);
|
|
4781
|
+
this.series.forEach((series, i) => {
|
|
4782
|
+
const value = series.values[index];
|
|
4783
|
+
if (value === null) return;
|
|
4784
|
+
const color = String(i % COLORS + 1);
|
|
4785
|
+
const top = series.tops[index];
|
|
4786
|
+
if (top !== null && this._points) {
|
|
4787
|
+
const dot = document.createElement("span");
|
|
4788
|
+
dot.className = series.className;
|
|
4789
|
+
dot.dataset.series = color;
|
|
4790
|
+
dot.style.top = `${series.y(top)}%`;
|
|
4791
|
+
this._cursor.append(dot);
|
|
4792
|
+
}
|
|
4793
|
+
const name = document.createElement("span");
|
|
4794
|
+
name.dataset.series = color;
|
|
4795
|
+
name.textContent = series.name;
|
|
4796
|
+
const text = document.createElement("span");
|
|
4797
|
+
text.textContent = series.text[index];
|
|
4798
|
+
this._tooltip.append(name, text);
|
|
4799
|
+
});
|
|
4800
|
+
this._place(index);
|
|
4801
|
+
}
|
|
4802
|
+
/**
|
|
4803
|
+
* Moves the filled cursor and tooltip to a row: beside its band, with the
|
|
4804
|
+
* tooltip opening back over the plot past the middle.
|
|
4805
|
+
*/
|
|
4806
|
+
_place(index) {
|
|
4807
|
+
const x = this._x(index);
|
|
4808
|
+
const side = this._horizontal ? "top" : "left";
|
|
4809
|
+
this._cursor.style[side] = this._tooltip.style[side] = `${x}%`;
|
|
4810
|
+
this._tooltip.classList.toggle("end", x > 50);
|
|
4811
|
+
}
|
|
4812
|
+
_read() {
|
|
4813
|
+
const table = this.el.querySelector(":scope > table");
|
|
4814
|
+
if (!table) return;
|
|
4815
|
+
const headerRow = table.tHead?.rows[0] ?? table.rows[0];
|
|
4816
|
+
const header = Array.from(headerRow?.cells ?? []);
|
|
4817
|
+
const rows = Array.from(table.tBodies).flatMap((body) => Array.from(body.rows)).filter((row) => row !== headerRow);
|
|
4818
|
+
[this._header, this._rows] = [header, rows];
|
|
4819
|
+
this.labels = rows.map((row) => row.cells[0]?.textContent.trim() ?? "");
|
|
4820
|
+
this.series = header.slice(1).map((cell, i) => {
|
|
4821
|
+
const values = rows.map((row) => cellValue(row.cells[i + 1]));
|
|
4822
|
+
return {
|
|
4823
|
+
name: cell.textContent.trim(),
|
|
4824
|
+
className: cell.className,
|
|
4825
|
+
text: rows.map((row) => row.cells[i + 1]?.textContent.trim() ?? ""),
|
|
4826
|
+
values,
|
|
4827
|
+
tops: values,
|
|
4828
|
+
y: void 0
|
|
4829
|
+
};
|
|
4830
|
+
});
|
|
4831
|
+
const stacked = this.el.classList.contains("stacked");
|
|
4832
|
+
if (stacked) {
|
|
4833
|
+
const totals = this.labels.map(() => 0);
|
|
4834
|
+
for (const series of this.series) {
|
|
4835
|
+
series.base = [...totals];
|
|
4836
|
+
series.tops = series.values.map((value, i) => totals[i] = value === null || totals[i] === null ? null : totals[i] + value);
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
if (this.series.every((series) => series.tops.every((value) => value === null))) this.series = [];
|
|
4840
|
+
const option = (name) => this._number(name) ?? this.options[name];
|
|
4841
|
+
this._y = this._scale(this.series, option("min"), option("max"));
|
|
4842
|
+
}
|
|
4843
|
+
/** A number from one of the chart's data attributes, or null. */
|
|
4844
|
+
_number(name) {
|
|
4845
|
+
const value = parseFloat(this.el.dataset[name] ?? "");
|
|
4846
|
+
return Number.isFinite(value) ? value : null;
|
|
4847
|
+
}
|
|
4848
|
+
/**
|
|
4849
|
+
* Fits a y scale to the series' tops, between `min` and `max` where they
|
|
4850
|
+
* are set, and gives it to each series.
|
|
4851
|
+
*/
|
|
4852
|
+
_scale(series, min, max, fromZero = this._fromZero) {
|
|
4853
|
+
const stacked = this.el.classList.contains("stacked");
|
|
4854
|
+
let [low, high] = [Infinity, -Infinity];
|
|
4855
|
+
for (const { tops } of series) {
|
|
4856
|
+
for (const value of tops) {
|
|
4857
|
+
if (value === null) continue;
|
|
4858
|
+
low = Math.min(low, value);
|
|
4859
|
+
high = Math.max(high, value);
|
|
4860
|
+
}
|
|
4861
|
+
}
|
|
4862
|
+
if (low > high) low = high = 0;
|
|
4863
|
+
if (fromZero) {
|
|
4864
|
+
const [bottom, top] = [Math.min(0, low), Math.max(0, high)];
|
|
4865
|
+
const room = (top - (min ?? bottom)) * 0.1 || 1;
|
|
4866
|
+
min ?? (min = bottom < 0 ? bottom - room : 0);
|
|
4867
|
+
max ?? (max = top + room);
|
|
4868
|
+
} else {
|
|
4869
|
+
const pad = (high - low) * 0.1 || 1;
|
|
4870
|
+
min ?? (min = stacked ? Math.min(0, low) : low - pad);
|
|
4871
|
+
max ?? (max = high + (stacked ? (high - min) * 0.1 || 1 : pad));
|
|
4872
|
+
}
|
|
4873
|
+
const y = (value) => 100 - (value - min) / (max - min || 1) * 100;
|
|
4874
|
+
for (const each of series) each.y = y;
|
|
4875
|
+
return y;
|
|
4876
|
+
}
|
|
4877
|
+
/** Each row sits in the middle of an equal band, beside its label. */
|
|
4878
|
+
_x(index) {
|
|
4879
|
+
const x = (index + 0.5) / this.labels.length * 100;
|
|
4880
|
+
return this._reversed ? 100 - x : x;
|
|
4881
|
+
}
|
|
4882
|
+
_render() {
|
|
4883
|
+
const type = this._type;
|
|
4884
|
+
this._plot = document.createElement("div");
|
|
4885
|
+
this._plot.className = `${type}-plot`;
|
|
4886
|
+
const caption = this.el.querySelector(":scope > figcaption")?.textContent.trim();
|
|
4887
|
+
this._plot.setAttribute("role", "group");
|
|
4888
|
+
this._plot.setAttribute("aria-roledescription", type.replace(/-/g, " "));
|
|
4889
|
+
if (caption) this._plot.setAttribute("aria-label", caption);
|
|
4890
|
+
this._cursor = document.createElement("div");
|
|
4891
|
+
this._cursor.className = `${type}-cursor`;
|
|
4892
|
+
this._cursor.hidden = true;
|
|
4893
|
+
this._tooltip = document.createElement("div");
|
|
4894
|
+
this._tooltip.className = `${type}-tooltip`;
|
|
4895
|
+
this._tooltip.hidden = true;
|
|
4896
|
+
this._tooltip.setAttribute("aria-live", "polite");
|
|
4897
|
+
this._plot.append(this._draw(), this._cursor, this._tooltip);
|
|
4898
|
+
this._generated = [this._plot, ...this._guides()];
|
|
4899
|
+
this.el.append(...this._generated);
|
|
4900
|
+
}
|
|
4901
|
+
/**
|
|
4902
|
+
* The <tfoot> row as a total to show over the plot, its value over its
|
|
4903
|
+
* label, or null without one.
|
|
4904
|
+
*/
|
|
4905
|
+
_total(className) {
|
|
4906
|
+
const footer = this.el.querySelector(":scope > table")?.tFoot?.rows[0];
|
|
4907
|
+
if (!footer) return null;
|
|
4908
|
+
const total = document.createElement("div");
|
|
4909
|
+
total.className = className;
|
|
4910
|
+
const [label, value] = [document.createElement("span"), document.createElement("strong")];
|
|
4911
|
+
label.textContent = footer.cells[0]?.textContent.trim() ?? "";
|
|
4912
|
+
value.textContent = footer.cells[1]?.textContent.trim() ?? "";
|
|
4913
|
+
total.append(value, label);
|
|
4914
|
+
return total;
|
|
4915
|
+
}
|
|
4916
|
+
/** The parts below the plot: the row labels and the legend. */
|
|
4917
|
+
_guides() {
|
|
4918
|
+
const labels = document.createElement("ol");
|
|
4919
|
+
labels.className = `${this._type}-labels`;
|
|
4920
|
+
labels.setAttribute("aria-hidden", "true");
|
|
4921
|
+
for (const text of this.labels) {
|
|
4922
|
+
const li = document.createElement("li");
|
|
4923
|
+
li.textContent = text;
|
|
4924
|
+
labels.append(li);
|
|
4925
|
+
}
|
|
4926
|
+
return [labels, ...this._legend()];
|
|
4927
|
+
}
|
|
4928
|
+
/** The series legend, for more than one series. */
|
|
4929
|
+
_legend() {
|
|
4930
|
+
if (this.series.length < 2) return [];
|
|
4931
|
+
const legend = document.createElement("ul");
|
|
4932
|
+
legend.className = `${this._type}-legend`;
|
|
4933
|
+
legend.setAttribute("aria-hidden", "true");
|
|
4934
|
+
legend.append(...this.series.map((series, i) => {
|
|
4935
|
+
const li = document.createElement("li");
|
|
4936
|
+
li.dataset.series = String(i % COLORS + 1);
|
|
4937
|
+
li.textContent = series.name;
|
|
4938
|
+
return li;
|
|
4939
|
+
}));
|
|
4940
|
+
return [legend];
|
|
4941
|
+
}
|
|
4942
|
+
/** Draws the series, which fill the plot. */
|
|
4943
|
+
_draw(drawn = this.series) {
|
|
4944
|
+
const svg = document.createElementNS(SVG, "svg");
|
|
4945
|
+
svg.setAttribute("viewBox", "0 0 100 100");
|
|
4946
|
+
svg.setAttribute("preserveAspectRatio", "none");
|
|
4947
|
+
svg.setAttribute("aria-hidden", "true");
|
|
4948
|
+
svg.setAttribute("focusable", "false");
|
|
4949
|
+
for (const series of drawn) {
|
|
4950
|
+
const group = document.createElementNS(SVG, "g");
|
|
4951
|
+
group.setAttribute("class", `${this._type}-series ${series.className}`.trim());
|
|
4952
|
+
group.dataset.series = String(this.series.indexOf(series) % COLORS + 1);
|
|
4953
|
+
const runs = [[]];
|
|
4954
|
+
series.tops.forEach((value, index) => {
|
|
4955
|
+
if (value === null) runs.push([]);
|
|
4956
|
+
else runs[runs.length - 1].push(index);
|
|
4957
|
+
});
|
|
4958
|
+
const filled = runs.filter((run) => run.length);
|
|
4959
|
+
const points = (run, values) => run.map((index) => [this._x(index), series.y(values[index])]);
|
|
4960
|
+
const tops = filled.map((run) => monotonePath(points(run, series.tops)));
|
|
4961
|
+
const id = `line-chart-gradient-${++_gradientId}`;
|
|
4962
|
+
const gradient = document.createElementNS(SVG, "linearGradient");
|
|
4963
|
+
gradient.id = id;
|
|
4964
|
+
gradient.setAttribute("x2", "0");
|
|
4965
|
+
gradient.setAttribute("y2", "1");
|
|
4966
|
+
gradient.append(document.createElementNS(SVG, "stop"), document.createElementNS(SVG, "stop"));
|
|
4967
|
+
gradient.lastElementChild.setAttribute("offset", "1");
|
|
4968
|
+
const area = document.createElementNS(SVG, "path");
|
|
4969
|
+
area.setAttribute("class", `${this._type}-area`);
|
|
4970
|
+
area.setAttribute("fill", `url(#${id})`);
|
|
4971
|
+
area.setAttribute("d", filled.map((run, r) => {
|
|
4972
|
+
if (!series.base) return `${tops[r]}V100H${this._x(run[0])}Z`;
|
|
4973
|
+
const base = points(run, series.base).map(([x, y]) => [x, Math.min(100, y)]);
|
|
4974
|
+
return `${tops[r]}L${monotonePath(base.reverse()).slice(1)}Z`;
|
|
4975
|
+
}).join(""));
|
|
4976
|
+
const line = document.createElementNS(SVG, "path");
|
|
4977
|
+
line.setAttribute("class", `${this._type}-line`);
|
|
4978
|
+
line.setAttribute("d", tops.join(""));
|
|
4979
|
+
group.append(gradient, area, line);
|
|
4980
|
+
svg.append(group);
|
|
4981
|
+
}
|
|
4982
|
+
return svg;
|
|
4983
|
+
}
|
|
4984
|
+
/** The row under the pointer, or -1. */
|
|
4985
|
+
_indexAt(e) {
|
|
4986
|
+
const box = this._plot.getBoundingClientRect();
|
|
4987
|
+
const fraction = this._horizontal ? (e.clientY - box.top) / box.height : (e.clientX - box.left) / box.width;
|
|
4988
|
+
const index = Math.floor((this._reversed ? 1 - fraction : fraction) * this.labels.length);
|
|
4989
|
+
return Math.min(this.labels.length - 1, Math.max(0, index));
|
|
4990
|
+
}
|
|
4991
|
+
/**
|
|
4992
|
+
* For a circular chart, the pointer's position from the plot's middle: a
|
|
4993
|
+
* fraction of a turn clockwise from the top, and its distance as a share
|
|
4994
|
+
* of the plot's width.
|
|
4995
|
+
*/
|
|
4996
|
+
_polarAt(e) {
|
|
4997
|
+
const box = this._plot.getBoundingClientRect();
|
|
4998
|
+
const [x, y] = [e.clientX - box.left - box.width / 2, e.clientY - box.top - box.height / 2];
|
|
4999
|
+
return [(Math.atan2(x, -y) / (2 * Math.PI) + 1) % 1, Math.hypot(x, y) / box.width];
|
|
5000
|
+
}
|
|
5001
|
+
/** The keys that move to the next row and to the one before. */
|
|
5002
|
+
get _arrows() {
|
|
5003
|
+
if (this._circular) return [["ArrowRight", "ArrowDown"], ["ArrowLeft", "ArrowUp"]];
|
|
5004
|
+
if (this._horizontal) return [["ArrowDown"], ["ArrowUp"]];
|
|
5005
|
+
return this._reversed ? [["ArrowLeft"], ["ArrowRight"]] : [["ArrowRight"], ["ArrowLeft"]];
|
|
5006
|
+
}
|
|
5007
|
+
/** The index a key moves to, or undefined for a key the chart ignores. */
|
|
5008
|
+
_keyTarget(key) {
|
|
5009
|
+
const last = this.labels.length - 1;
|
|
5010
|
+
const [forward, back] = this._arrows;
|
|
5011
|
+
const step = (by) => {
|
|
5012
|
+
if (!this._circular) return Math.min(last, Math.max(0, this.activeIndex + by));
|
|
5013
|
+
if (this.activeIndex < 0) return by > 0 ? 0 : last;
|
|
5014
|
+
return (this.activeIndex + by + last + 1) % (last + 1);
|
|
5015
|
+
};
|
|
5016
|
+
if (key === "Escape") return this.activeIndex < 0 ? void 0 : -1;
|
|
5017
|
+
return forward.includes(key) ? step(1) : back.includes(key) ? step(-1) : { Home: 0, End: last }[key];
|
|
5018
|
+
}
|
|
5019
|
+
};
|
|
5020
|
+
|
|
5021
|
+
// src/ts/components/columnChart.ts
|
|
5022
|
+
var ColumnChart = class extends LineChart {
|
|
5023
|
+
get _type() {
|
|
5024
|
+
return "column-chart";
|
|
5025
|
+
}
|
|
5026
|
+
get _key() {
|
|
5027
|
+
return "Expressive_ColumnChart";
|
|
5028
|
+
}
|
|
5029
|
+
get _fromZero() {
|
|
5030
|
+
return true;
|
|
5031
|
+
}
|
|
5032
|
+
get _points() {
|
|
5033
|
+
return false;
|
|
5034
|
+
}
|
|
5035
|
+
static init(els, options = {}) {
|
|
5036
|
+
return super.init(els, options);
|
|
5037
|
+
}
|
|
5038
|
+
static getInstance(el) {
|
|
5039
|
+
return el["Expressive_ColumnChart"];
|
|
5040
|
+
}
|
|
5041
|
+
_draw(drawn = this.series) {
|
|
5042
|
+
const columns = document.createElement("div");
|
|
5043
|
+
columns.className = "column-chart-columns";
|
|
5044
|
+
columns.setAttribute("aria-hidden", "true");
|
|
5045
|
+
const clamp = (value, y = drawn[0]?.y ?? this._y) => Math.min(100, Math.max(0, y(value)));
|
|
5046
|
+
columns.style.setProperty("--zero", `${clamp(0)}%`);
|
|
5047
|
+
this._plot.style.setProperty("--rows", String(this.labels.length));
|
|
5048
|
+
this.labels.forEach((_, index) => {
|
|
5049
|
+
const row = document.createElement("div");
|
|
5050
|
+
drawn.forEach((series) => {
|
|
5051
|
+
const to = series.tops[index];
|
|
5052
|
+
const from = series.base?.[index] ?? 0;
|
|
5053
|
+
if (series.base && (to === null || from === null)) return;
|
|
5054
|
+
const column = document.createElement("span");
|
|
5055
|
+
if (to !== null) {
|
|
5056
|
+
column.className = series.className;
|
|
5057
|
+
column.dataset.series = String(this.series.indexOf(series) % COLORS + 1);
|
|
5058
|
+
column.classList.toggle("negative", !series.base && to < from);
|
|
5059
|
+
column.style.setProperty("--top", `${clamp(Math.max(from, to), series.y)}%`);
|
|
5060
|
+
column.style.setProperty("--bottom", `${100 - clamp(Math.min(from, to), series.y)}%`);
|
|
5061
|
+
if (this._horizontal) column.dataset.text = series.text[index];
|
|
5062
|
+
}
|
|
5063
|
+
row.append(column);
|
|
5064
|
+
});
|
|
5065
|
+
columns.append(row);
|
|
5066
|
+
});
|
|
5067
|
+
return columns;
|
|
5068
|
+
}
|
|
5069
|
+
};
|
|
5070
|
+
|
|
5071
|
+
// src/ts/components/barChart.ts
|
|
5072
|
+
var BarChart = class extends ColumnChart {
|
|
5073
|
+
get _type() {
|
|
5074
|
+
return "bar-chart";
|
|
5075
|
+
}
|
|
5076
|
+
get _key() {
|
|
5077
|
+
return "Expressive_BarChart";
|
|
5078
|
+
}
|
|
5079
|
+
get _horizontal() {
|
|
5080
|
+
return true;
|
|
5081
|
+
}
|
|
5082
|
+
static init(els, options = {}) {
|
|
5083
|
+
return super.init(els, options);
|
|
5084
|
+
}
|
|
5085
|
+
static getInstance(el) {
|
|
5086
|
+
return el["Expressive_BarChart"];
|
|
5087
|
+
}
|
|
5088
|
+
/**
|
|
5089
|
+
* The column chart's spans; CSS reads their --top and --bottom across the
|
|
5090
|
+
* plot. With .values, a copy of the bars without their clipping holds each
|
|
5091
|
+
* cell's text, so text longer than a short bar can run past its end.
|
|
5092
|
+
*/
|
|
5093
|
+
_draw() {
|
|
5094
|
+
const bars = super._draw();
|
|
5095
|
+
bars.className = "bar-chart-bars";
|
|
5096
|
+
bars.classList.toggle("rtl", getComputedStyle(this.el).direction === "rtl");
|
|
5097
|
+
if (!this.el.classList.contains("values")) return bars;
|
|
5098
|
+
const values = bars.cloneNode(true);
|
|
5099
|
+
values.classList.replace("bar-chart-bars", "bar-chart-values");
|
|
5100
|
+
values.querySelectorAll("span[data-text]").forEach((span) => {
|
|
5101
|
+
span.textContent = span.dataset.text;
|
|
5102
|
+
});
|
|
5103
|
+
const layers = document.createElement("div");
|
|
5104
|
+
layers.className = "bar-chart-layers";
|
|
5105
|
+
layers.append(bars, values);
|
|
5106
|
+
return layers;
|
|
5107
|
+
}
|
|
5108
|
+
};
|
|
5109
|
+
|
|
5110
|
+
// src/ts/components/pieChart.ts
|
|
5111
|
+
var COLORS2 = 6;
|
|
5112
|
+
var RADIUS = 46;
|
|
5113
|
+
var GROWN = 1.06;
|
|
5114
|
+
var LABEL_SHARE = 0.05;
|
|
5115
|
+
var _maskId = 0;
|
|
5116
|
+
var PieChart = class extends LineChart {
|
|
5117
|
+
get _type() {
|
|
5118
|
+
return "pie-chart";
|
|
5119
|
+
}
|
|
5120
|
+
get _key() {
|
|
5121
|
+
return "Expressive_PieChart";
|
|
5122
|
+
}
|
|
5123
|
+
get _points() {
|
|
5124
|
+
return false;
|
|
5125
|
+
}
|
|
5126
|
+
get _circular() {
|
|
5127
|
+
return true;
|
|
5128
|
+
}
|
|
5129
|
+
static init(els, options = {}) {
|
|
5130
|
+
return super.init(els, options);
|
|
5131
|
+
}
|
|
5132
|
+
static getInstance(el) {
|
|
5133
|
+
return el["Expressive_PieChart"];
|
|
5134
|
+
}
|
|
5135
|
+
show(index) {
|
|
5136
|
+
if (!this._tooltip || index === this.activeIndex) return;
|
|
5137
|
+
super.show(index);
|
|
5138
|
+
this._slices.forEach((slice, i) => slice?.classList.toggle("active", i === index));
|
|
5139
|
+
if (this._tooltip.hidden) return;
|
|
5140
|
+
const color = this._colors[index];
|
|
5141
|
+
this._tooltip.querySelectorAll("[data-series]").forEach((span) => {
|
|
5142
|
+
if (color) span.dataset.series = color;
|
|
5143
|
+
else delete span.dataset.series;
|
|
5144
|
+
});
|
|
5145
|
+
const share = this._share(index);
|
|
5146
|
+
const value = this._tooltip.querySelector("span:nth-of-type(2)");
|
|
5147
|
+
if (share && value) value.textContent += ` (${share})`;
|
|
5148
|
+
}
|
|
5149
|
+
/** CSS places the tooltip on the slice's middle from --turn. */
|
|
5150
|
+
_place(index) {
|
|
5151
|
+
const middle = (this._turns[index][0] + this._turns[index][1]) / 2;
|
|
5152
|
+
this._tooltip.style.setProperty("--turn", String(middle));
|
|
5153
|
+
this._tooltip.classList.toggle("end", middle < 0.5);
|
|
5154
|
+
}
|
|
5155
|
+
/** The row's share of the whole as a percentage, or '' for none. */
|
|
5156
|
+
_share(index) {
|
|
5157
|
+
const [start, end] = this._turns[index];
|
|
5158
|
+
if (end <= start) return "";
|
|
5159
|
+
const share = end - start;
|
|
5160
|
+
const options = { style: "percent", maximumFractionDigits: share < 0.1 ? 1 : 0 };
|
|
5161
|
+
try {
|
|
5162
|
+
return share.toLocaleString(this.el.closest("[lang]")?.lang || void 0, options);
|
|
5163
|
+
} catch {
|
|
5164
|
+
return share.toLocaleString(void 0, options);
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
/** The legend: every row with its color, its cell and its share. */
|
|
5168
|
+
_guides() {
|
|
5169
|
+
const legend = document.createElement("ul");
|
|
5170
|
+
legend.className = "pie-chart-legend";
|
|
5171
|
+
legend.setAttribute("aria-hidden", "true");
|
|
5172
|
+
const text = this.series[0].text;
|
|
5173
|
+
legend.append(...this.labels.map((label, i) => {
|
|
5174
|
+
const li = document.createElement("li");
|
|
5175
|
+
if (this._colors[i]) li.dataset.series = this._colors[i];
|
|
5176
|
+
const [name, value, share] = [label, text[i], this._share(i)].map((content) => {
|
|
5177
|
+
const span = document.createElement("span");
|
|
5178
|
+
span.textContent = content;
|
|
5179
|
+
return span;
|
|
5180
|
+
});
|
|
5181
|
+
li.append(name, value, share);
|
|
5182
|
+
return li;
|
|
5183
|
+
}));
|
|
5184
|
+
return [legend];
|
|
5185
|
+
}
|
|
5186
|
+
/** The row under the pointer by its angle from the middle, or -1 off the slices. */
|
|
5187
|
+
_indexAt(e) {
|
|
5188
|
+
const [turn, share] = this._polarAt(e);
|
|
5189
|
+
const distance = share * 100;
|
|
5190
|
+
const hole = this.el.classList.contains("donut") ? parseFloat(getComputedStyle(this.el).getPropertyValue("--md-comp-pie-chart-hole")) || 0 : 0;
|
|
5191
|
+
if (distance < RADIUS * hole) return -1;
|
|
5192
|
+
const index = this._turns.findIndex(([start, end]) => start <= turn && turn < end);
|
|
5193
|
+
return distance > RADIUS * (index === this.activeIndex ? GROWN : 1) ? -1 : index;
|
|
5194
|
+
}
|
|
5195
|
+
/**
|
|
5196
|
+
* An SVG of slices in a -50 to 50 box, under a mask that cuts the gaps
|
|
5197
|
+
* between them and the donut's hole, which CSS sizes. The values and the
|
|
5198
|
+
* total are HTML over it.
|
|
5199
|
+
*/
|
|
5200
|
+
_draw() {
|
|
5201
|
+
const values = this.series[0].values.map((value) => Math.max(0, value ?? 0));
|
|
5202
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
5203
|
+
let turn = 0;
|
|
5204
|
+
this._turns = values.map((value) => [turn, turn += total ? value / total : 0]);
|
|
5205
|
+
const drawn = this._turns.filter(([start, end]) => end > start).length;
|
|
5206
|
+
let slice = 0;
|
|
5207
|
+
this._colors = this._turns.map(([start, end]) => {
|
|
5208
|
+
if (end <= start) return void 0;
|
|
5209
|
+
const index = slice++;
|
|
5210
|
+
return String(index === drawn - 1 && index > 0 && index % COLORS2 === 0 ? 2 : index % COLORS2 + 1);
|
|
5211
|
+
});
|
|
5212
|
+
const at = (t, r = RADIUS) => {
|
|
5213
|
+
const angle = t * 2 * Math.PI;
|
|
5214
|
+
return `${(r * Math.sin(angle)).toFixed(3)},${(-r * Math.cos(angle)).toFixed(3)}`;
|
|
5215
|
+
};
|
|
5216
|
+
const pie = document.createElement("div");
|
|
5217
|
+
pie.className = "pie-chart-pie";
|
|
5218
|
+
pie.setAttribute("aria-hidden", "true");
|
|
5219
|
+
const svg = svgNode("svg", { viewBox: "-50 -50 100 100", focusable: "false" });
|
|
5220
|
+
const id = `pie-chart-mask-${++_maskId}`;
|
|
5221
|
+
const area = { x: "-50", y: "-50", width: "100", height: "100" };
|
|
5222
|
+
const mask = svgNode("mask", { id, maskUnits: "userSpaceOnUse", ...area });
|
|
5223
|
+
const edges = this._turns.filter(([start, end]) => end > start);
|
|
5224
|
+
mask.append(
|
|
5225
|
+
svgNode("rect", { ...area, fill: "white" }),
|
|
5226
|
+
svgNode("path", {
|
|
5227
|
+
class: "pie-chart-gaps",
|
|
5228
|
+
stroke: "black",
|
|
5229
|
+
d: edges.length > 1 ? edges.map(([start]) => `M0,0L${at(start, 50)}`).join("") : ""
|
|
5230
|
+
}),
|
|
5231
|
+
svgNode("circle", { class: "pie-chart-hole", r: String(RADIUS), fill: "black" })
|
|
5232
|
+
);
|
|
5233
|
+
const slices = svgNode("g", { mask: `url(#${id})` });
|
|
5234
|
+
const arc = `A${RADIUS},${RADIUS} 0`;
|
|
5235
|
+
this._slices = this._turns.map(([start, end], i) => {
|
|
5236
|
+
if (end <= start) return null;
|
|
5237
|
+
const slice2 = svgNode("path", {
|
|
5238
|
+
class: "pie-chart-slice",
|
|
5239
|
+
"data-series": this._colors[i],
|
|
5240
|
+
// A whole circle is two half arcs; one arc cannot end where it starts.
|
|
5241
|
+
d: end - start >= 1 ? `M0,${-RADIUS}${arc} 1 1 0,${RADIUS}${arc} 1 1 0,${-RADIUS}Z` : `M0,0L${at(start)}${arc} ${end - start > 0.5 ? 1 : 0} 1 ${at(end)}Z`
|
|
5242
|
+
});
|
|
5243
|
+
slices.append(slice2);
|
|
5244
|
+
return slice2;
|
|
5245
|
+
});
|
|
5246
|
+
svg.append(mask, slices);
|
|
5247
|
+
pie.append(svg);
|
|
5248
|
+
if (this.el.classList.contains("values")) {
|
|
5249
|
+
const labels = document.createElement("div");
|
|
5250
|
+
labels.className = "pie-chart-values";
|
|
5251
|
+
this._turns.forEach(([start, end], i) => {
|
|
5252
|
+
if (end - start < LABEL_SHARE) return;
|
|
5253
|
+
const label = document.createElement("span");
|
|
5254
|
+
label.dataset.series = this._colors[i];
|
|
5255
|
+
label.style.setProperty("--turn", String((start + end) / 2));
|
|
5256
|
+
label.textContent = this._share(i);
|
|
5257
|
+
labels.append(label);
|
|
5258
|
+
});
|
|
5259
|
+
pie.append(labels);
|
|
5260
|
+
}
|
|
5261
|
+
const middle = this._total("pie-chart-total");
|
|
5262
|
+
if (middle) pie.append(middle);
|
|
5263
|
+
return pie;
|
|
5264
|
+
}
|
|
5265
|
+
};
|
|
5266
|
+
|
|
5267
|
+
// src/ts/components/heatmapChart.ts
|
|
5268
|
+
var HeatmapChart = class extends LineChart {
|
|
5269
|
+
get _type() {
|
|
5270
|
+
return "heatmap-chart";
|
|
5271
|
+
}
|
|
5272
|
+
get _key() {
|
|
5273
|
+
return "Expressive_HeatmapChart";
|
|
5274
|
+
}
|
|
5275
|
+
static init(els, options = {}) {
|
|
5276
|
+
return super.init(els, options);
|
|
5277
|
+
}
|
|
5278
|
+
static getInstance(el) {
|
|
5279
|
+
return el["Expressive_HeatmapChart"];
|
|
5280
|
+
}
|
|
5281
|
+
/** Highlights one cell, numbered row by row. Pass -1 to hide the tooltip. */
|
|
5282
|
+
show(index) {
|
|
5283
|
+
if (!this._tooltip || index === this.activeIndex) return;
|
|
5284
|
+
this._cells[this.activeIndex]?.classList.remove("active");
|
|
5285
|
+
this.activeIndex = index;
|
|
5286
|
+
const cell = this._cells[index];
|
|
5287
|
+
this._tooltip.hidden = !cell;
|
|
5288
|
+
if (!cell) return;
|
|
5289
|
+
cell.classList.add("active");
|
|
5290
|
+
const columns = this.series.length;
|
|
5291
|
+
const [row, series] = [Math.floor(index / columns), this.series[index % columns]];
|
|
5292
|
+
const title = document.createElement("strong");
|
|
5293
|
+
title.textContent = this.labels[row];
|
|
5294
|
+
const [name, text] = [document.createElement("span"), document.createElement("span")];
|
|
5295
|
+
name.textContent = series.name;
|
|
5296
|
+
if (this._levels[index] === null) name.className = "gap";
|
|
5297
|
+
else name.style.setProperty("--level", String(this._levels[index]));
|
|
5298
|
+
text.textContent = series.text[row];
|
|
5299
|
+
this._tooltip.replaceChildren(title, name, text);
|
|
5300
|
+
const end = cell.offsetLeft + cell.offsetWidth / 2 > this._plot.clientWidth / 2;
|
|
5301
|
+
this._tooltip.classList.toggle("end", end);
|
|
5302
|
+
this._tooltip.style.left = `${cell.offsetLeft + (end ? 0 : cell.offsetWidth)}px`;
|
|
5303
|
+
this._tooltip.style.top = `${cell.offsetTop}px`;
|
|
5304
|
+
}
|
|
5305
|
+
/** Arrows move the way they point, so left and right swap in RTL. */
|
|
5306
|
+
_keyTarget(key) {
|
|
5307
|
+
if (key === "Escape") return this.activeIndex < 0 ? void 0 : -1;
|
|
5308
|
+
const [columns, last] = [this.series.length, this.labels.length - 1];
|
|
5309
|
+
const index = Math.max(0, this.activeIndex);
|
|
5310
|
+
const [row, column] = [Math.floor(index / columns), index % columns];
|
|
5311
|
+
const [next, back] = this._reversed ? ["ArrowLeft", "ArrowRight"] : ["ArrowRight", "ArrowLeft"];
|
|
5312
|
+
const to = {
|
|
5313
|
+
[next]: [row, column + 1],
|
|
5314
|
+
[back]: [row, column - 1],
|
|
5315
|
+
ArrowDown: [row + 1, column],
|
|
5316
|
+
ArrowUp: [row - 1, column],
|
|
5317
|
+
Home: [row, 0],
|
|
5318
|
+
End: [row, columns - 1]
|
|
5319
|
+
}[key];
|
|
5320
|
+
if (!to) return void 0;
|
|
5321
|
+
if (this.activeIndex < 0) return 0;
|
|
5322
|
+
const clamp = (value, max) => Math.min(max, Math.max(0, value));
|
|
5323
|
+
return clamp(to[0], last) * columns + clamp(to[1], columns - 1);
|
|
5324
|
+
}
|
|
5325
|
+
/**
|
|
5326
|
+
* The cell under the pointer by its position, or -1 off the grid. A touch
|
|
5327
|
+
* keeps its first target while it drags, so the target cannot be used. The
|
|
5328
|
+
* cells and gaps are even, so each band's edge falls in a gap and a gap
|
|
5329
|
+
* counts as the nearer cell.
|
|
5330
|
+
*/
|
|
5331
|
+
_indexAt(e) {
|
|
5332
|
+
const [columns, rows] = [this.series.length, this.labels.length];
|
|
5333
|
+
const first = this._cells[0].getBoundingClientRect();
|
|
5334
|
+
const last = this._cells[this._cells.length - 1].getBoundingClientRect();
|
|
5335
|
+
const [left, right] = [Math.min(first.left, last.left), Math.max(first.right, last.right)];
|
|
5336
|
+
const [x, y] = [e.clientX, e.clientY];
|
|
5337
|
+
if (x < left || x > right || y < first.top || y > last.bottom) return -1;
|
|
5338
|
+
const band = (offset, size, count) => Math.min(count - 1, Math.floor(offset / (size || 1) * count));
|
|
5339
|
+
const column = band(x - left, right - left, columns);
|
|
5340
|
+
return band(y - first.top, last.bottom - first.top, rows) * columns + (this._reversed ? columns - 1 - column : column);
|
|
5341
|
+
}
|
|
5342
|
+
/**
|
|
5343
|
+
* The legend: a gradient from the lowest cell's shade to the highest's,
|
|
5344
|
+
* labelled with their text. A fixed scale wider than the data shows only
|
|
5345
|
+
* the part the data uses.
|
|
5346
|
+
*/
|
|
5347
|
+
_guides() {
|
|
5348
|
+
const legend = document.createElement("div");
|
|
5349
|
+
legend.className = "heatmap-chart-legend";
|
|
5350
|
+
legend.setAttribute("aria-hidden", "true");
|
|
5351
|
+
const [low, scale, high] = ["span", "span", "span"].map((tag) => document.createElement(tag));
|
|
5352
|
+
[low.textContent, high.textContent] = this._ends;
|
|
5353
|
+
scale.className = "heatmap-chart-scale";
|
|
5354
|
+
if (this._reversed) scale.style.setProperty("--heatmap-chart-to", "left");
|
|
5355
|
+
scale.style.setProperty("--low", String(this._endLevels[0]));
|
|
5356
|
+
scale.style.setProperty("--high", String(this._endLevels[1]));
|
|
5357
|
+
legend.append(low, scale, high);
|
|
5358
|
+
return [legend];
|
|
5359
|
+
}
|
|
5360
|
+
/**
|
|
5361
|
+
* A grid of the row headers and cells, row by row, with the column headers
|
|
5362
|
+
* below. Each cell's --level shades it in CSS.
|
|
5363
|
+
*/
|
|
5364
|
+
_draw() {
|
|
5365
|
+
const columns = this.series.length;
|
|
5366
|
+
let [low, high] = [Infinity, -Infinity];
|
|
5367
|
+
this._ends = ["", ""];
|
|
5368
|
+
this.series.forEach((series) => series.values.forEach((value, row) => {
|
|
5369
|
+
if (value === null) return;
|
|
5370
|
+
if (value < low) [low, this._ends[0]] = [value, series.text[row]];
|
|
5371
|
+
if (value > high) [high, this._ends[1]] = [value, series.text[row]];
|
|
5372
|
+
}));
|
|
5373
|
+
const option = (name) => {
|
|
5374
|
+
const value = parseFloat(this.el.dataset[name] ?? "");
|
|
5375
|
+
return Number.isFinite(value) ? value : this.options[name];
|
|
5376
|
+
};
|
|
5377
|
+
const [min, max] = [option("min") ?? low, option("max") ?? high];
|
|
5378
|
+
const shade = (value) => Math.min(1, Math.max(0, (value - min) / (max - min || 1)));
|
|
5379
|
+
this._endLevels = [shade(low), shade(high)];
|
|
5380
|
+
const grid = document.createElement("div");
|
|
5381
|
+
grid.className = "heatmap-chart-grid";
|
|
5382
|
+
grid.setAttribute("aria-hidden", "true");
|
|
5383
|
+
grid.style.setProperty("--columns", String(columns));
|
|
5384
|
+
grid.style.setProperty("--rows", String(this.labels.length));
|
|
5385
|
+
const span = (className, text = "") => {
|
|
5386
|
+
const element = document.createElement("span");
|
|
5387
|
+
element.className = className;
|
|
5388
|
+
element.textContent = text;
|
|
5389
|
+
return element;
|
|
5390
|
+
};
|
|
5391
|
+
const label = (cell) => cell?.dataset.label ?? cell?.textContent.trim() ?? "";
|
|
5392
|
+
const values = this.el.classList.contains("values");
|
|
5393
|
+
this._cells = [];
|
|
5394
|
+
this._levels = [];
|
|
5395
|
+
this.labels.forEach((_, row) => {
|
|
5396
|
+
grid.append(span("heatmap-chart-row", label(this._rows[row].cells[0])));
|
|
5397
|
+
this.series.forEach((series) => {
|
|
5398
|
+
const value = series.values[row];
|
|
5399
|
+
const level = value === null ? null : shade(value);
|
|
5400
|
+
const cell = span("heatmap-chart-cell", values ? series.text[row] : "");
|
|
5401
|
+
if (level === null) cell.classList.add("gap");
|
|
5402
|
+
else cell.style.setProperty("--level", String(level));
|
|
5403
|
+
this._cells.push(cell);
|
|
5404
|
+
this._levels.push(level);
|
|
5405
|
+
grid.append(cell);
|
|
5406
|
+
});
|
|
5407
|
+
});
|
|
5408
|
+
const labels = this._header.slice(1, columns + 1).map(label);
|
|
5409
|
+
labels.forEach((text, column) => {
|
|
5410
|
+
if (!text) return;
|
|
5411
|
+
let end = column + 1;
|
|
5412
|
+
while (end < columns && !labels[end]) end++;
|
|
5413
|
+
const element = span("heatmap-chart-column", text);
|
|
5414
|
+
element.classList.toggle("run", end > column + 1);
|
|
5415
|
+
element.style.gridArea = `${this.labels.length + 1} / ${column + 2} / auto / ${end + 2}`;
|
|
5416
|
+
grid.append(element);
|
|
5417
|
+
});
|
|
5418
|
+
return grid;
|
|
5419
|
+
}
|
|
5420
|
+
};
|
|
5421
|
+
|
|
5422
|
+
// src/ts/components/radarChart.ts
|
|
5423
|
+
var RINGS = 4;
|
|
5424
|
+
var RadarChart = class extends LineChart {
|
|
5425
|
+
get _type() {
|
|
5426
|
+
return "radar-chart";
|
|
5427
|
+
}
|
|
5428
|
+
get _key() {
|
|
5429
|
+
return "Expressive_RadarChart";
|
|
5430
|
+
}
|
|
5431
|
+
/** The scale starts at zero, the middle, so shapes compare by size. */
|
|
5432
|
+
get _fromZero() {
|
|
5433
|
+
return true;
|
|
5434
|
+
}
|
|
5435
|
+
get _circular() {
|
|
5436
|
+
return true;
|
|
5437
|
+
}
|
|
5438
|
+
static init(els, options = {}) {
|
|
5439
|
+
return super.init(els, options);
|
|
5440
|
+
}
|
|
5441
|
+
static getInstance(el) {
|
|
5442
|
+
return el["Expressive_RadarChart"];
|
|
5443
|
+
}
|
|
5444
|
+
/**
|
|
5445
|
+
* The cursor is a spoke turned by --turn, and the tooltip sits past its
|
|
5446
|
+
* tip. LineChart's points on the spoke keep their top, a share of the
|
|
5447
|
+
* radius, which stops at the rim and the middle like the shapes.
|
|
5448
|
+
*/
|
|
5449
|
+
_place(index) {
|
|
5450
|
+
for (const dot of this._cursor.children) {
|
|
5451
|
+
dot.style.top = `${Math.min(100, Math.max(0, parseFloat(dot.style.top)))}%`;
|
|
5452
|
+
}
|
|
5453
|
+
const turn = this._turn(index);
|
|
5454
|
+
this._cursor.style.setProperty("--turn", String(turn));
|
|
5455
|
+
this._tooltip.style.setProperty("--turn", String(turn));
|
|
5456
|
+
this._tooltip.classList.toggle("end", turn > 0 && turn < 0.5);
|
|
5457
|
+
}
|
|
5458
|
+
/** A row's spoke as a fraction of a turn clockwise from the top. */
|
|
5459
|
+
_turn(index) {
|
|
5460
|
+
return index / this.labels.length;
|
|
5461
|
+
}
|
|
5462
|
+
/** Only the legend; the labels go round the rim in _draw. */
|
|
5463
|
+
_guides() {
|
|
5464
|
+
return this._legend();
|
|
5465
|
+
}
|
|
5466
|
+
/** The nearest spoke to the pointer, by its angle from the middle. */
|
|
5467
|
+
_indexAt(e) {
|
|
5468
|
+
const [turn] = this._polarAt(e);
|
|
5469
|
+
return Math.round(turn * this.labels.length) % this.labels.length;
|
|
5470
|
+
}
|
|
5471
|
+
/**
|
|
5472
|
+
* An SVG in a -50 to 50 box: the rings and spokes of the grid, then each
|
|
5473
|
+
* series' shape, with its points for .points. The rim is the box's edge;
|
|
5474
|
+
* the labels round it are HTML.
|
|
5475
|
+
*/
|
|
5476
|
+
_draw() {
|
|
5477
|
+
const at = (index, share2) => {
|
|
5478
|
+
const angle = this._turn(index) * 2 * Math.PI;
|
|
5479
|
+
return `${(50 * share2 * Math.sin(angle)).toFixed(3)},${(-50 * share2 * Math.cos(angle)).toFixed(3)}`;
|
|
5480
|
+
};
|
|
5481
|
+
const share = (value) => Math.min(1, Math.max(0, 1 - this._y(value) / 100));
|
|
5482
|
+
const rows = this.labels.map((_, i) => i);
|
|
5483
|
+
const ring = (r) => `M${rows.map((i) => at(i, r)).join("L")}Z`;
|
|
5484
|
+
const svg = svgNode("svg", { viewBox: "-50 -50 100 100", "aria-hidden": "true", focusable: "false" });
|
|
5485
|
+
svg.append(svgNode("path", {
|
|
5486
|
+
class: "radar-chart-grid",
|
|
5487
|
+
d: Array.from({ length: RINGS }, (_, r) => ring((r + 1) / RINGS)).join("") + rows.map((i) => `M0,0L${at(i, 1)}`).join("")
|
|
5488
|
+
}));
|
|
5489
|
+
const points = this.el.classList.contains("points");
|
|
5490
|
+
this.series.forEach((series, s) => {
|
|
5491
|
+
const group = svgNode("g", {
|
|
5492
|
+
class: `radar-chart-series ${series.className}`.trim(),
|
|
5493
|
+
"data-series": String(s % COLORS + 1)
|
|
5494
|
+
});
|
|
5495
|
+
const drawn = rows.filter((i) => series.values[i] !== null);
|
|
5496
|
+
const corners = drawn.map((i) => at(i, share(series.values[i])));
|
|
5497
|
+
group.append(svgNode("path", { class: "radar-chart-shape", d: corners.length ? `M${corners.join("L")}Z` : "" }));
|
|
5498
|
+
if (points) group.append(svgNode("path", { class: "radar-chart-points", d: corners.map((c) => `M${c}h0`).join("") }));
|
|
5499
|
+
svg.append(group);
|
|
5500
|
+
});
|
|
5501
|
+
const labels = document.createElement("ol");
|
|
5502
|
+
labels.className = "radar-chart-axes";
|
|
5503
|
+
labels.setAttribute("aria-hidden", "true");
|
|
5504
|
+
this.labels.forEach((text, i) => {
|
|
5505
|
+
const li = document.createElement("li");
|
|
5506
|
+
li.textContent = text;
|
|
5507
|
+
li.style.setProperty("--turn", String(this._turn(i)));
|
|
5508
|
+
labels.append(li);
|
|
5509
|
+
});
|
|
5510
|
+
const web = document.createElement("div");
|
|
5511
|
+
web.className = "radar-chart-web";
|
|
5512
|
+
web.append(svg, labels);
|
|
5513
|
+
return web;
|
|
5514
|
+
}
|
|
5515
|
+
};
|
|
5516
|
+
|
|
5517
|
+
// src/ts/components/radialChart.ts
|
|
5518
|
+
var RADIUS2 = 46;
|
|
5519
|
+
var FILL = 0.8;
|
|
5520
|
+
var RadialChart = class extends LineChart {
|
|
5521
|
+
get _type() {
|
|
5522
|
+
return "radial-chart";
|
|
5523
|
+
}
|
|
5524
|
+
get _key() {
|
|
5525
|
+
return "Expressive_RadialChart";
|
|
5526
|
+
}
|
|
5527
|
+
get _points() {
|
|
5528
|
+
return false;
|
|
5529
|
+
}
|
|
5530
|
+
get _circular() {
|
|
5531
|
+
return true;
|
|
5532
|
+
}
|
|
5533
|
+
get _gauge() {
|
|
5534
|
+
return this.el.classList.contains("gauge");
|
|
5535
|
+
}
|
|
5536
|
+
static init(els, options = {}) {
|
|
5537
|
+
return super.init(els, options);
|
|
5538
|
+
}
|
|
5539
|
+
static getInstance(el) {
|
|
5540
|
+
return el["Expressive_RadialChart"];
|
|
5541
|
+
}
|
|
5542
|
+
show(index) {
|
|
5543
|
+
if (!this._tooltip || index === this.activeIndex) return;
|
|
5544
|
+
super.show(index);
|
|
5545
|
+
this._rings.forEach((ring, i) => ring.classList.toggle("active", i === index));
|
|
5546
|
+
if (this._tooltip.hidden) return;
|
|
5547
|
+
const color = this._color(index);
|
|
5548
|
+
this._tooltip.querySelectorAll("[data-series]").forEach((span) => {
|
|
5549
|
+
span.dataset.series = color;
|
|
5550
|
+
});
|
|
5551
|
+
}
|
|
5552
|
+
/** The tooltip sits where the ring's fill ends. */
|
|
5553
|
+
_place(index) {
|
|
5554
|
+
const turn = this._turn(this._fractions[index]);
|
|
5555
|
+
const r = this._radii[index];
|
|
5556
|
+
const [x, y] = [r * Math.sin(turn * 2 * Math.PI), -r * Math.cos(turn * 2 * Math.PI)];
|
|
5557
|
+
this._tooltip.style.left = `${50 + x}%`;
|
|
5558
|
+
this._tooltip.style.top = `${(50 + y) / (this._gauge ? 50 : 100) * 100}%`;
|
|
5559
|
+
this._tooltip.classList.toggle("end", x > 0);
|
|
5560
|
+
}
|
|
5561
|
+
_color(index) {
|
|
5562
|
+
return String(index % COLORS + 1);
|
|
5563
|
+
}
|
|
5564
|
+
/** A fraction of the ring as a turn clockwise from the top. */
|
|
5565
|
+
_turn(fraction) {
|
|
5566
|
+
return this._gauge ? fraction / 2 - 0.25 : fraction;
|
|
5567
|
+
}
|
|
5568
|
+
/** For more than one row, a legend of every row with its color and cell. */
|
|
5569
|
+
_guides() {
|
|
5570
|
+
if (this.labels.length < 2) return [];
|
|
5571
|
+
const legend = document.createElement("ul");
|
|
5572
|
+
legend.className = "radial-chart-legend";
|
|
5573
|
+
legend.setAttribute("aria-hidden", "true");
|
|
5574
|
+
const text = this.series[0].text;
|
|
5575
|
+
legend.append(...this.labels.map((label, i) => {
|
|
5576
|
+
const li = document.createElement("li");
|
|
5577
|
+
li.dataset.series = this._color(i);
|
|
5578
|
+
const [name, value] = [label, text[i]].map((content) => {
|
|
5579
|
+
const span = document.createElement("span");
|
|
5580
|
+
span.textContent = content;
|
|
5581
|
+
return span;
|
|
5582
|
+
});
|
|
5583
|
+
li.append(name, value);
|
|
5584
|
+
return li;
|
|
5585
|
+
}));
|
|
5586
|
+
return [legend];
|
|
5587
|
+
}
|
|
5588
|
+
/** The ring under the pointer by its distance from the middle, or -1. */
|
|
5589
|
+
_indexAt(e) {
|
|
5590
|
+
const box = this._plot.getBoundingClientRect();
|
|
5591
|
+
const [x, y] = [e.clientX - box.left - box.width / 2, e.clientY - box.top - box.width / 2];
|
|
5592
|
+
if (this._gauge && y > 0) return -1;
|
|
5593
|
+
const distance = Math.hypot(x, y) / box.width * 100;
|
|
5594
|
+
const step = this._radii.length > 1 ? this._radii[0] - this._radii[1] : this._width;
|
|
5595
|
+
return this._radii.findIndex((r) => Math.abs(distance - r) <= step / 2);
|
|
5596
|
+
}
|
|
5597
|
+
/**
|
|
5598
|
+
* An SVG of rings in a -50 to 50 box, the top half for a gauge. Each row
|
|
5599
|
+
* is a track circle and a fill circle whose dash is its share; CSS draws
|
|
5600
|
+
* them from --radial-chart-dash. The total is HTML over it.
|
|
5601
|
+
*/
|
|
5602
|
+
_draw() {
|
|
5603
|
+
const style = getComputedStyle(this.el);
|
|
5604
|
+
const token = (name, fallback) => {
|
|
5605
|
+
const value = parseFloat(style.getPropertyValue(`--md-comp-radial-chart-${name}`));
|
|
5606
|
+
return Number.isFinite(value) ? value : fallback;
|
|
5607
|
+
};
|
|
5608
|
+
const count = this.labels.length;
|
|
5609
|
+
let [width, gap] = [Math.max(0, token("thickness", 8)), Math.max(0, token("gap", 2))];
|
|
5610
|
+
const scale = Math.min(1, RADIUS2 * FILL / (count * width + (count - 1) * gap || 1));
|
|
5611
|
+
[width, gap] = [width * scale, gap * scale];
|
|
5612
|
+
this._width = width;
|
|
5613
|
+
this._radii = this.labels.map((_, i) => RADIUS2 - width / 2 - i * (width + gap));
|
|
5614
|
+
const max = [parseFloat(this.el.dataset.max ?? ""), this.options.max ?? NaN, 100].find((value) => value > 0);
|
|
5615
|
+
this._fractions = this.series[0].values.map((value) => Math.min(1, Math.max(0, (value ?? 0) / max)));
|
|
5616
|
+
const gauge = this._gauge;
|
|
5617
|
+
const svg = svgNode("svg", { viewBox: gauge ? "-50 -50 100 50" : "-50 -50 100 100", focusable: "false" });
|
|
5618
|
+
const transform = gauge ? "rotate(180)" : "rotate(-90)";
|
|
5619
|
+
const span = gauge ? 0.5 : 1;
|
|
5620
|
+
this._rings = this._radii.map((r, i) => {
|
|
5621
|
+
const length = 2 * Math.PI * r;
|
|
5622
|
+
const circle = (name, fraction, capped) => {
|
|
5623
|
+
const ring2 = svgNode("circle", { class: name, r: r.toFixed(3), transform, "stroke-width": width.toFixed(3) });
|
|
5624
|
+
const dash = Math.max(1e-3, fraction * span * length - (capped ? width : 0));
|
|
5625
|
+
ring2.style.setProperty("--radial-chart-dash", `${dash.toFixed(3)} ${length.toFixed(3)}`);
|
|
5626
|
+
if (capped) ring2.style.setProperty("--radial-chart-offset", (-width / 2).toFixed(3));
|
|
5627
|
+
return ring2;
|
|
5628
|
+
};
|
|
5629
|
+
const ring = circle("radial-chart-ring", this._fractions[i], true);
|
|
5630
|
+
ring.dataset.series = this._color(i);
|
|
5631
|
+
if (!this._fractions[i]) ring.classList.add("empty");
|
|
5632
|
+
svg.append(circle("radial-chart-track", 1, gauge), ring);
|
|
5633
|
+
return ring;
|
|
5634
|
+
});
|
|
5635
|
+
const rings = document.createElement("div");
|
|
5636
|
+
rings.className = "radial-chart-rings";
|
|
5637
|
+
rings.setAttribute("aria-hidden", "true");
|
|
5638
|
+
rings.style.setProperty("--radial-chart-middle", `${(this._radii[count - 1] - this._width / 2) * 2}%`);
|
|
5639
|
+
rings.append(svg);
|
|
5640
|
+
const total = this._total("radial-chart-total");
|
|
5641
|
+
if (total) rings.append(total);
|
|
5642
|
+
return rings;
|
|
5643
|
+
}
|
|
5644
|
+
};
|
|
5645
|
+
|
|
5646
|
+
// src/ts/components/mixedChart.ts
|
|
5647
|
+
var has = (series, name) => series.className.split(/\s+/).includes(name);
|
|
5648
|
+
var MixedChart = class extends ColumnChart {
|
|
5649
|
+
get _type() {
|
|
5650
|
+
return "mixed-chart";
|
|
5651
|
+
}
|
|
5652
|
+
get _key() {
|
|
5653
|
+
return "Expressive_MixedChart";
|
|
5654
|
+
}
|
|
5655
|
+
get _points() {
|
|
5656
|
+
return true;
|
|
5657
|
+
}
|
|
5658
|
+
static init(els, options = {}) {
|
|
5659
|
+
return super.init(els, options);
|
|
5660
|
+
}
|
|
5661
|
+
static getInstance(el) {
|
|
5662
|
+
return el["Expressive_MixedChart"];
|
|
5663
|
+
}
|
|
5664
|
+
/** The start scale, then the end scale; each includes zero if it has columns. */
|
|
5665
|
+
_scale(series, min, max) {
|
|
5666
|
+
const end = series.filter((each) => has(each, "end"));
|
|
5667
|
+
const start = series.filter((each) => !end.includes(each));
|
|
5668
|
+
const columns = (list) => list.some((each) => has(each, "column"));
|
|
5669
|
+
super._scale(end, this._number("endMin"), this._number("endMax"), columns(end));
|
|
5670
|
+
return super._scale(start, min, max, columns(start));
|
|
5671
|
+
}
|
|
5672
|
+
/** Columns behind, then the lines and areas over them. */
|
|
5673
|
+
_draw() {
|
|
5674
|
+
const marks = document.createElement("div");
|
|
5675
|
+
marks.className = "mixed-chart-marks";
|
|
5676
|
+
const columns = super._draw(this.series.filter((series) => has(series, "column")));
|
|
5677
|
+
columns.className = "mixed-chart-columns";
|
|
5678
|
+
marks.append(columns, LineChart.prototype["_draw"].call(this, this.series.filter((series) => !has(series, "column"))));
|
|
5679
|
+
return marks;
|
|
5680
|
+
}
|
|
5681
|
+
};
|
|
5682
|
+
|
|
5683
|
+
// src/ts/components/select.ts
|
|
5684
|
+
var _defaults15 = {
|
|
3922
5685
|
classes: "",
|
|
3923
5686
|
menuOptions: {}
|
|
3924
5687
|
};
|
|
@@ -4000,7 +5763,7 @@ var FormSelect = class _FormSelect extends Component {
|
|
|
4000
5763
|
this._setupEventHandlers();
|
|
4001
5764
|
}
|
|
4002
5765
|
static get defaults() {
|
|
4003
|
-
return
|
|
5766
|
+
return _defaults15;
|
|
4004
5767
|
}
|
|
4005
5768
|
/**
|
|
4006
5769
|
* Initializes instances of FormSelect.
|
|
@@ -4425,7 +6188,7 @@ var FormSelect = class _FormSelect extends Component {
|
|
|
4425
6188
|
};
|
|
4426
6189
|
|
|
4427
6190
|
// src/ts/plugins/dockedDisplayPlugin.ts
|
|
4428
|
-
var
|
|
6191
|
+
var _defaults16 = {
|
|
4429
6192
|
margin: 5,
|
|
4430
6193
|
transition: 10,
|
|
4431
6194
|
duration: 250,
|
|
@@ -4474,7 +6237,7 @@ var DockedDisplayPlugin = class _DockedDisplayPlugin {
|
|
|
4474
6237
|
};
|
|
4475
6238
|
this.el = el;
|
|
4476
6239
|
this.options = {
|
|
4477
|
-
...
|
|
6240
|
+
..._defaults16,
|
|
4478
6241
|
...options
|
|
4479
6242
|
};
|
|
4480
6243
|
this.container = document.createElement("div");
|
|
@@ -4495,7 +6258,7 @@ var DockedDisplayPlugin = class _DockedDisplayPlugin {
|
|
|
4495
6258
|
};
|
|
4496
6259
|
|
|
4497
6260
|
// src/ts/components/datepicker.ts
|
|
4498
|
-
var
|
|
6261
|
+
var _defaults17 = {
|
|
4499
6262
|
// the default output format for the input field value
|
|
4500
6263
|
format: "mmm dd, yyyy",
|
|
4501
6264
|
// Used to create date object from current input string
|
|
@@ -4780,7 +6543,7 @@ var _Datepicker = class _Datepicker extends Component {
|
|
|
4780
6543
|
}
|
|
4781
6544
|
}
|
|
4782
6545
|
static get defaults() {
|
|
4783
|
-
return
|
|
6546
|
+
return _defaults17;
|
|
4784
6547
|
}
|
|
4785
6548
|
/**
|
|
4786
6549
|
* Initializes instances of Datepicker.
|
|
@@ -5595,8 +7358,402 @@ _Datepicker._template = `
|
|
|
5595
7358
|
</div>`;
|
|
5596
7359
|
var Datepicker = _Datepicker;
|
|
5597
7360
|
|
|
7361
|
+
// src/ts/components/messageScroller.ts
|
|
7362
|
+
var _defaults18 = {
|
|
7363
|
+
autoScroll: true,
|
|
7364
|
+
defaultScrollPosition: "end",
|
|
7365
|
+
peek: 64
|
|
7366
|
+
};
|
|
7367
|
+
var EDGE = 2;
|
|
7368
|
+
var UP_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "PageUp", "Home"]);
|
|
7369
|
+
var RELEASE_EVENTS = ["pointerup", "pointercancel", "touchend", "touchcancel"];
|
|
7370
|
+
var MessageScroller = class _MessageScroller extends Component {
|
|
7371
|
+
constructor(el, options) {
|
|
7372
|
+
super(el, options, _MessageScroller);
|
|
7373
|
+
this._opened = false;
|
|
7374
|
+
this._following = false;
|
|
7375
|
+
/** A press is down: it may be a tap or a scrollbar drag. */
|
|
7376
|
+
this._holding = false;
|
|
7377
|
+
this._lastTop = 0;
|
|
7378
|
+
/** Where a programmatic scroll is heading; null when none is running. */
|
|
7379
|
+
this._target = null;
|
|
7380
|
+
/** Distance left to the target at the last scroll event. */
|
|
7381
|
+
this._distance = Infinity;
|
|
7382
|
+
/** The first visible message and its offset from the viewport top. */
|
|
7383
|
+
this._ref = null;
|
|
7384
|
+
this._refOffset = 0;
|
|
7385
|
+
/** The latest anchored turn, which the spacer lets reach the top. */
|
|
7386
|
+
this._anchor = null;
|
|
7387
|
+
this._spacer = 0;
|
|
7388
|
+
this._last = null;
|
|
7389
|
+
this._resizeObserver = null;
|
|
7390
|
+
this._mutationObserver = null;
|
|
7391
|
+
// A programmatic scroll ends when it arrives, or when a scroll moves away
|
|
7392
|
+
// from its target because something else took over.
|
|
7393
|
+
this._handleScroll = () => {
|
|
7394
|
+
if (this._target !== null) {
|
|
7395
|
+
const distance = Math.abs(this.viewport.scrollTop - this._resolve(this._target));
|
|
7396
|
+
if (distance > EDGE && distance < this._distance) {
|
|
7397
|
+
this._distance = distance;
|
|
7398
|
+
return;
|
|
7399
|
+
}
|
|
7400
|
+
this._target = null;
|
|
7401
|
+
}
|
|
7402
|
+
this._settle();
|
|
7403
|
+
};
|
|
7404
|
+
this._handleUserScroll = (e) => {
|
|
7405
|
+
if (e.type === "wheel" && e.deltaY >= 0) return;
|
|
7406
|
+
if (e.type === "keydown" && !UP_KEYS.has(e.key)) return;
|
|
7407
|
+
this._target = null;
|
|
7408
|
+
if (e.type === "pointerdown" || e.type === "touchstart") this._holding = true;
|
|
7409
|
+
else this._following = false;
|
|
7410
|
+
};
|
|
7411
|
+
this._handleRelease = () => {
|
|
7412
|
+
if (!this._holding) return;
|
|
7413
|
+
this._holding = false;
|
|
7414
|
+
if (this._following) this.viewport.scrollTop = this.viewport.scrollHeight;
|
|
7415
|
+
this._sync();
|
|
7416
|
+
};
|
|
7417
|
+
this._handleButtonClick = () => this.scrollToEnd();
|
|
7418
|
+
this._handleMutations = (records) => {
|
|
7419
|
+
for (const record of records) {
|
|
7420
|
+
record.addedNodes.forEach((node) => {
|
|
7421
|
+
if (node instanceof Element) this._resizeObserver?.observe(node);
|
|
7422
|
+
});
|
|
7423
|
+
record.removedNodes.forEach((node) => {
|
|
7424
|
+
if (node instanceof Element) this._resizeObserver?.unobserve(node);
|
|
7425
|
+
});
|
|
7426
|
+
}
|
|
7427
|
+
const previous = this._last;
|
|
7428
|
+
this._last = this.viewport.lastElementChild;
|
|
7429
|
+
if (!this._opened) return;
|
|
7430
|
+
if (!this._following && this._target === null) this._restore();
|
|
7431
|
+
if (!this.options.autoScroll) return;
|
|
7432
|
+
if (previous && previous.parentElement !== this.viewport) return;
|
|
7433
|
+
for (let item = this._last; item && item !== previous; item = item.previousElementSibling) {
|
|
7434
|
+
if (item.hasAttribute("data-scroll-anchor")) {
|
|
7435
|
+
this._anchor = item;
|
|
7436
|
+
this._updateSpacer();
|
|
7437
|
+
this._following = true;
|
|
7438
|
+
this._scrollTo(this._top(item) - this.options.peek);
|
|
7439
|
+
return;
|
|
7440
|
+
}
|
|
7441
|
+
}
|
|
7442
|
+
};
|
|
7443
|
+
this._handleResize = () => {
|
|
7444
|
+
if (!this._opened) {
|
|
7445
|
+
if (this.viewport.clientHeight > 0) this._open();
|
|
7446
|
+
return;
|
|
7447
|
+
}
|
|
7448
|
+
this._updateSpacer();
|
|
7449
|
+
const v = this.viewport;
|
|
7450
|
+
if (this._target === "end") this._scrollTo("end");
|
|
7451
|
+
else if (this._target === null) {
|
|
7452
|
+
if (this._following && !this._holding) v.scrollTop = v.scrollHeight;
|
|
7453
|
+
else this._restore();
|
|
7454
|
+
}
|
|
7455
|
+
this._sync();
|
|
7456
|
+
};
|
|
7457
|
+
this.el["Expressive_MessageScroller"] = this;
|
|
7458
|
+
this.options = {
|
|
7459
|
+
..._MessageScroller.defaults,
|
|
7460
|
+
...options
|
|
7461
|
+
};
|
|
7462
|
+
this.viewport = el.querySelector(":scope > .message-scroller-viewport");
|
|
7463
|
+
this.button = el.querySelector(":scope > .message-scroller-button");
|
|
7464
|
+
if (!this.viewport) {
|
|
7465
|
+
console.error("MessageScroller needs a .message-scroller-viewport child.");
|
|
7466
|
+
return;
|
|
7467
|
+
}
|
|
7468
|
+
this._last = this.viewport.lastElementChild;
|
|
7469
|
+
if (this.viewport.clientHeight > 0) this._open();
|
|
7470
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
7471
|
+
this._resizeObserver = new ResizeObserver(this._handleResize);
|
|
7472
|
+
this._resizeObserver.observe(this.viewport, { box: "border-box" });
|
|
7473
|
+
for (const item of Array.from(this.viewport.children)) this._resizeObserver.observe(item);
|
|
7474
|
+
}
|
|
7475
|
+
this._mutationObserver = new MutationObserver(this._handleMutations);
|
|
7476
|
+
this._mutationObserver.observe(this.viewport, { childList: true });
|
|
7477
|
+
this.viewport.addEventListener("scroll", this._handleScroll, { passive: true });
|
|
7478
|
+
for (const type of ["wheel", "touchstart", "pointerdown", "keydown"]) {
|
|
7479
|
+
this.viewport.addEventListener(type, this._handleUserScroll, { passive: true });
|
|
7480
|
+
}
|
|
7481
|
+
for (const type of RELEASE_EVENTS) document.addEventListener(type, this._handleRelease);
|
|
7482
|
+
this.button?.addEventListener("click", this._handleButtonClick);
|
|
7483
|
+
}
|
|
7484
|
+
static get defaults() {
|
|
7485
|
+
return _defaults18;
|
|
7486
|
+
}
|
|
7487
|
+
/**
|
|
7488
|
+
* Initializes instances of MessageScroller.
|
|
7489
|
+
* @param els HTML elements.
|
|
7490
|
+
* @param options Component options.
|
|
7491
|
+
*/
|
|
7492
|
+
static init(els, options = {}) {
|
|
7493
|
+
return super.init(els, options, _MessageScroller);
|
|
7494
|
+
}
|
|
7495
|
+
static getInstance(el) {
|
|
7496
|
+
return el["Expressive_MessageScroller"];
|
|
7497
|
+
}
|
|
7498
|
+
destroy() {
|
|
7499
|
+
this.el["Expressive_MessageScroller"] = void 0;
|
|
7500
|
+
if (!this.viewport) return;
|
|
7501
|
+
this._resizeObserver?.disconnect();
|
|
7502
|
+
this._mutationObserver?.disconnect();
|
|
7503
|
+
this.viewport.removeEventListener("scroll", this._handleScroll);
|
|
7504
|
+
for (const type of ["wheel", "touchstart", "pointerdown", "keydown"]) {
|
|
7505
|
+
this.viewport.removeEventListener(type, this._handleUserScroll);
|
|
7506
|
+
}
|
|
7507
|
+
for (const type of RELEASE_EVENTS) document.removeEventListener(type, this._handleRelease);
|
|
7508
|
+
this.viewport.style.removeProperty("--md-comp-message-scroller-spacer");
|
|
7509
|
+
if (this.button) {
|
|
7510
|
+
this.button.removeEventListener("click", this._handleButtonClick);
|
|
7511
|
+
this.button.inert = false;
|
|
7512
|
+
}
|
|
7513
|
+
}
|
|
7514
|
+
/** Scrolls to the latest message and resumes following. */
|
|
7515
|
+
scrollToEnd() {
|
|
7516
|
+
this._following = this.options.autoScroll;
|
|
7517
|
+
this._scrollTo("end");
|
|
7518
|
+
}
|
|
7519
|
+
/** Scrolls to the first message. */
|
|
7520
|
+
scrollToStart() {
|
|
7521
|
+
this._following = false;
|
|
7522
|
+
this._scrollTo(0);
|
|
7523
|
+
}
|
|
7524
|
+
/**
|
|
7525
|
+
* Scrolls the element with this id to the top of the viewport, below the
|
|
7526
|
+
* peek, and stops following.
|
|
7527
|
+
* @returns false when no such element is inside the viewport.
|
|
7528
|
+
*/
|
|
7529
|
+
scrollToMessage(id) {
|
|
7530
|
+
const target = Utils.getElementById(this.viewport, id);
|
|
7531
|
+
if (!target || !this.viewport.contains(target)) return false;
|
|
7532
|
+
this._following = false;
|
|
7533
|
+
this._scrollTo(this._top(target) - this.options.peek);
|
|
7534
|
+
return true;
|
|
7535
|
+
}
|
|
7536
|
+
_open() {
|
|
7537
|
+
this._opened = true;
|
|
7538
|
+
const v = this.viewport;
|
|
7539
|
+
const position = this.options.defaultScrollPosition;
|
|
7540
|
+
const anchors = v.querySelectorAll(":scope > [data-scroll-anchor]");
|
|
7541
|
+
const anchor = position === "last-anchor" ? anchors[anchors.length - 1] : null;
|
|
7542
|
+
if (position === "start") v.scrollTop = 0;
|
|
7543
|
+
else if (anchor && this._contentEnd() - this._top(anchor) + this.options.peek > v.clientHeight) {
|
|
7544
|
+
v.scrollTop = this._top(anchor) - this.options.peek;
|
|
7545
|
+
} else v.scrollTop = v.scrollHeight;
|
|
7546
|
+
this._settle();
|
|
7547
|
+
}
|
|
7548
|
+
/**
|
|
7549
|
+
* Records where the reader is once no programmatic scroll is running.
|
|
7550
|
+
* Content can grow between a scroll and the resize that re-pins the end,
|
|
7551
|
+
* so only moving toward the start releases following.
|
|
7552
|
+
*/
|
|
7553
|
+
_settle() {
|
|
7554
|
+
const top = this.viewport.scrollTop;
|
|
7555
|
+
const kept = this._following && top >= this._lastTop - EDGE;
|
|
7556
|
+
this._following = this.options.autoScroll && (kept || this._atEnd());
|
|
7557
|
+
this._lastTop = top;
|
|
7558
|
+
this._capture();
|
|
7559
|
+
this._sync();
|
|
7560
|
+
}
|
|
7561
|
+
_scrollTo(target) {
|
|
7562
|
+
const v = this.viewport;
|
|
7563
|
+
if (Math.abs(v.scrollTop - this._resolve(target)) <= EDGE) {
|
|
7564
|
+
this._target = null;
|
|
7565
|
+
this._settle();
|
|
7566
|
+
return;
|
|
7567
|
+
}
|
|
7568
|
+
this._target = target;
|
|
7569
|
+
this._distance = Infinity;
|
|
7570
|
+
const reduce = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
7571
|
+
v.scrollTo({
|
|
7572
|
+
top: target === "end" ? v.scrollHeight : target,
|
|
7573
|
+
behavior: reduce ? "auto" : "smooth"
|
|
7574
|
+
});
|
|
7575
|
+
}
|
|
7576
|
+
_resolve(target) {
|
|
7577
|
+
const max = this.viewport.scrollHeight - this.viewport.clientHeight;
|
|
7578
|
+
return target === "end" ? max : Math.max(0, Math.min(target, max));
|
|
7579
|
+
}
|
|
7580
|
+
_atEnd() {
|
|
7581
|
+
const v = this.viewport;
|
|
7582
|
+
return v.scrollHeight - v.scrollTop - v.clientHeight <= EDGE;
|
|
7583
|
+
}
|
|
7584
|
+
/** Offset of an element from the top of the scrolled content. */
|
|
7585
|
+
_top(el) {
|
|
7586
|
+
const v = this.viewport;
|
|
7587
|
+
return el.getBoundingClientRect().top - v.getBoundingClientRect().top - v.clientTop + v.scrollTop;
|
|
7588
|
+
}
|
|
7589
|
+
/** Bottom of the content without the spacer, padding included. */
|
|
7590
|
+
_contentEnd() {
|
|
7591
|
+
const last = this.viewport.lastElementChild;
|
|
7592
|
+
if (!last) return 0;
|
|
7593
|
+
const padding = parseFloat(getComputedStyle(this.viewport).paddingBlockEnd) - this._spacer;
|
|
7594
|
+
return this._top(last) + last.getBoundingClientRect().height + padding;
|
|
7595
|
+
}
|
|
7596
|
+
/** Remembers the first visible message so prepends can keep it in place. */
|
|
7597
|
+
_capture() {
|
|
7598
|
+
const items = this.viewport.children;
|
|
7599
|
+
const top = this.viewport.getBoundingClientRect().top + this.viewport.clientTop;
|
|
7600
|
+
let low = 0;
|
|
7601
|
+
let high = items.length - 1;
|
|
7602
|
+
while (low < high) {
|
|
7603
|
+
const mid = low + high >> 1;
|
|
7604
|
+
if (items[mid].getBoundingClientRect().bottom > top) high = mid;
|
|
7605
|
+
else low = mid + 1;
|
|
7606
|
+
}
|
|
7607
|
+
this._ref = items[low] ?? null;
|
|
7608
|
+
this._refOffset = this._ref ? this._ref.getBoundingClientRect().top - top : 0;
|
|
7609
|
+
}
|
|
7610
|
+
/** Moves the remembered message back to where the reader last saw it. */
|
|
7611
|
+
_restore() {
|
|
7612
|
+
if (this._ref?.parentElement !== this.viewport) {
|
|
7613
|
+
this._capture();
|
|
7614
|
+
return;
|
|
7615
|
+
}
|
|
7616
|
+
const top = this.viewport.getBoundingClientRect().top + this.viewport.clientTop;
|
|
7617
|
+
const delta = this._ref.getBoundingClientRect().top - top - this._refOffset;
|
|
7618
|
+
if (Math.abs(delta) >= 1) this.viewport.scrollTop += delta;
|
|
7619
|
+
}
|
|
7620
|
+
/** Sizes the blank space that lets the latest anchored turn reach the top. */
|
|
7621
|
+
_updateSpacer() {
|
|
7622
|
+
let space = 0;
|
|
7623
|
+
if (this._anchor?.parentElement === this.viewport) {
|
|
7624
|
+
const room = this._top(this._anchor) - this.options.peek + this.viewport.clientHeight;
|
|
7625
|
+
space = Math.max(0, Math.ceil(room - this._contentEnd()));
|
|
7626
|
+
} else this._anchor = null;
|
|
7627
|
+
if (space === this._spacer) return;
|
|
7628
|
+
this._spacer = space;
|
|
7629
|
+
if (space) this.viewport.style.setProperty("--md-comp-message-scroller-spacer", `${space}px`);
|
|
7630
|
+
else this.viewport.style.removeProperty("--md-comp-message-scroller-spacer");
|
|
7631
|
+
}
|
|
7632
|
+
/** The button is inert while there is nothing below. */
|
|
7633
|
+
_sync() {
|
|
7634
|
+
if (!this.button) return;
|
|
7635
|
+
const idle = this._atEnd();
|
|
7636
|
+
const root = this.button.getRootNode();
|
|
7637
|
+
if (idle && root.activeElement === this.button) this.viewport.focus({ preventScroll: true });
|
|
7638
|
+
this.button.inert = idle;
|
|
7639
|
+
}
|
|
7640
|
+
};
|
|
7641
|
+
|
|
7642
|
+
// src/ts/components/richTextEditor.ts
|
|
7643
|
+
var _defaults19 = {
|
|
7644
|
+
Editor: null,
|
|
7645
|
+
extensions: []
|
|
7646
|
+
};
|
|
7647
|
+
var NAMING = ["aria-label", "aria-labelledby", "aria-describedby"];
|
|
7648
|
+
var RichTextEditor = class _RichTextEditor extends Component {
|
|
7649
|
+
constructor(el, options) {
|
|
7650
|
+
super(el, options, _RichTextEditor);
|
|
7651
|
+
/** The Tiptap editor. */
|
|
7652
|
+
this.editor = null;
|
|
7653
|
+
/** The element the editor is mounted in. */
|
|
7654
|
+
this.content = null;
|
|
7655
|
+
this._actions = /* @__PURE__ */ new Map();
|
|
7656
|
+
this._label = null;
|
|
7657
|
+
this._handleClick = (e) => {
|
|
7658
|
+
const button = e.target.closest("button[data-command]");
|
|
7659
|
+
const action = this._actions.get(button);
|
|
7660
|
+
if (action && !button.disabled) this.editor.chain().focus()[action.command](action.attributes).run();
|
|
7661
|
+
};
|
|
7662
|
+
this._handleLabelClick = () => {
|
|
7663
|
+
this.editor.chain().focus().run();
|
|
7664
|
+
};
|
|
7665
|
+
this._handleUpdate = () => {
|
|
7666
|
+
this.textarea.value = this.editor.isEmpty ? "" : this.editor.getHTML();
|
|
7667
|
+
this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
7668
|
+
};
|
|
7669
|
+
this._handleTransaction = () => {
|
|
7670
|
+
const can = this.editor.isEditable && this.editor.can();
|
|
7671
|
+
for (const [button, { command, attributes, active }] of this._actions) {
|
|
7672
|
+
button.disabled = !can || !(command in this.editor.commands) || !can[command](attributes);
|
|
7673
|
+
if (!active) continue;
|
|
7674
|
+
const on = this.editor.isActive(active, attributes);
|
|
7675
|
+
button.setAttribute("aria-pressed", String(on));
|
|
7676
|
+
button.classList.toggle("active", on);
|
|
7677
|
+
}
|
|
7678
|
+
};
|
|
7679
|
+
this.el["Expressive_RichTextEditor"] = this;
|
|
7680
|
+
this.options = {
|
|
7681
|
+
..._RichTextEditor.defaults,
|
|
7682
|
+
...options
|
|
7683
|
+
};
|
|
7684
|
+
this.textarea = el.querySelector(":scope > textarea");
|
|
7685
|
+
if (!this.textarea || !this.options.Editor) {
|
|
7686
|
+
console.error("RichTextEditor needs a <textarea> child and the Tiptap Editor class in options.Editor.");
|
|
7687
|
+
return;
|
|
7688
|
+
}
|
|
7689
|
+
for (const button of el.querySelectorAll("button[data-command]")) {
|
|
7690
|
+
const command = button.dataset.command;
|
|
7691
|
+
const attributes2 = button.dataset.attributes ? JSON.parse(button.dataset.attributes) : void 0;
|
|
7692
|
+
const active = command.startsWith("toggle") ? command[6].toLowerCase() + command.slice(7) : void 0;
|
|
7693
|
+
this._actions.set(button, { command, attributes: attributes2, active });
|
|
7694
|
+
}
|
|
7695
|
+
const attributes = { role: "textbox", "aria-multiline": "true" };
|
|
7696
|
+
for (const name of NAMING) {
|
|
7697
|
+
const value = this.textarea.getAttribute(name);
|
|
7698
|
+
if (value) attributes[name] = value;
|
|
7699
|
+
}
|
|
7700
|
+
this._label = this.textarea.labels?.[0] ?? null;
|
|
7701
|
+
if (this._label && !attributes["aria-labelledby"]) {
|
|
7702
|
+
if (!this._label.id) this._label.id = `rich-text-editor-label-${Utils.guid()}`;
|
|
7703
|
+
attributes["aria-labelledby"] = this._label.id;
|
|
7704
|
+
this._label.addEventListener("click", this._handleLabelClick);
|
|
7705
|
+
}
|
|
7706
|
+
this.content = document.createElement("div");
|
|
7707
|
+
this.content.className = "rich-text-editor-content";
|
|
7708
|
+
this.textarea.after(this.content);
|
|
7709
|
+
this.textarea.hidden = true;
|
|
7710
|
+
this.editor = new this.options.Editor({
|
|
7711
|
+
element: this.content,
|
|
7712
|
+
extensions: this.options.extensions,
|
|
7713
|
+
content: this.textarea.value,
|
|
7714
|
+
editable: !(this.textarea.disabled || this.textarea.readOnly),
|
|
7715
|
+
editorProps: { attributes }
|
|
7716
|
+
});
|
|
7717
|
+
this.editor.on("update", this._handleUpdate);
|
|
7718
|
+
this.editor.on("transaction", this._handleTransaction);
|
|
7719
|
+
this._handleTransaction();
|
|
7720
|
+
el.addEventListener("click", this._handleClick);
|
|
7721
|
+
}
|
|
7722
|
+
static get defaults() {
|
|
7723
|
+
return _defaults19;
|
|
7724
|
+
}
|
|
7725
|
+
/**
|
|
7726
|
+
* Initializes instances of RichTextEditor.
|
|
7727
|
+
* @param els HTML elements.
|
|
7728
|
+
* @param options Component options.
|
|
7729
|
+
*/
|
|
7730
|
+
static init(els, options = {}) {
|
|
7731
|
+
return super.init(els, options, _RichTextEditor);
|
|
7732
|
+
}
|
|
7733
|
+
static getInstance(el) {
|
|
7734
|
+
return el["Expressive_RichTextEditor"];
|
|
7735
|
+
}
|
|
7736
|
+
destroy() {
|
|
7737
|
+
this.el["Expressive_RichTextEditor"] = void 0;
|
|
7738
|
+
if (!this.editor) return;
|
|
7739
|
+
this.el.removeEventListener("click", this._handleClick);
|
|
7740
|
+
this._label?.removeEventListener("click", this._handleLabelClick);
|
|
7741
|
+
this.editor.off("update", this._handleUpdate);
|
|
7742
|
+
this.editor.off("transaction", this._handleTransaction);
|
|
7743
|
+
this.editor.destroy();
|
|
7744
|
+
this.editor = null;
|
|
7745
|
+
this.content.remove();
|
|
7746
|
+
this.textarea.hidden = false;
|
|
7747
|
+
for (const button of this._actions.keys()) {
|
|
7748
|
+
button.disabled = false;
|
|
7749
|
+
button.classList.remove("active");
|
|
7750
|
+
button.removeAttribute("aria-pressed");
|
|
7751
|
+
}
|
|
7752
|
+
}
|
|
7753
|
+
};
|
|
7754
|
+
|
|
5598
7755
|
// src/ts/components/lightbox.ts
|
|
5599
|
-
var
|
|
7756
|
+
var _defaults20 = {
|
|
5600
7757
|
inDuration: 275,
|
|
5601
7758
|
outDuration: 200,
|
|
5602
7759
|
onOpenStart: null,
|
|
@@ -5745,7 +7902,7 @@ var _Lightbox = class _Lightbox extends Component {
|
|
|
5745
7902
|
this._setupEventHandlers();
|
|
5746
7903
|
}
|
|
5747
7904
|
static get defaults() {
|
|
5748
|
-
return
|
|
7905
|
+
return _defaults20;
|
|
5749
7906
|
}
|
|
5750
7907
|
/**
|
|
5751
7908
|
* Initializes instances of Lightbox.
|
|
@@ -5951,7 +8108,7 @@ var Lightbox = _Lightbox;
|
|
|
5951
8108
|
|
|
5952
8109
|
// src/ts/components/scrollspy.ts
|
|
5953
8110
|
var _defaultGetActiveElement = (id) => `a[href="#${id}"]`;
|
|
5954
|
-
var
|
|
8111
|
+
var _defaults21 = {
|
|
5955
8112
|
scrollOffset: 200,
|
|
5956
8113
|
activeClass: "active",
|
|
5957
8114
|
getActiveElement: _defaultGetActiveElement,
|
|
@@ -5987,7 +8144,7 @@ var _ScrollSpy = class _ScrollSpy extends Component {
|
|
|
5987
8144
|
}
|
|
5988
8145
|
}
|
|
5989
8146
|
static get defaults() {
|
|
5990
|
-
return
|
|
8147
|
+
return _defaults21;
|
|
5991
8148
|
}
|
|
5992
8149
|
static init(els, options = {}) {
|
|
5993
8150
|
return super.init(els, options, _ScrollSpy);
|
|
@@ -6077,7 +8234,7 @@ _ScrollSpy._active = /* @__PURE__ */ new Map();
|
|
|
6077
8234
|
var ScrollSpy = _ScrollSpy;
|
|
6078
8235
|
|
|
6079
8236
|
// src/ts/components/navigationRail.ts
|
|
6080
|
-
var
|
|
8237
|
+
var _defaults22 = {
|
|
6081
8238
|
onOpenStart: null,
|
|
6082
8239
|
onOpenEnd: null,
|
|
6083
8240
|
onCloseStart: null,
|
|
@@ -6164,7 +8321,7 @@ var NavigationRail = class _NavigationRail extends Component {
|
|
|
6164
8321
|
document.addEventListener("keydown", this._onKeyDown);
|
|
6165
8322
|
}
|
|
6166
8323
|
static get defaults() {
|
|
6167
|
-
return
|
|
8324
|
+
return _defaults22;
|
|
6168
8325
|
}
|
|
6169
8326
|
static init(els, options = {}) {
|
|
6170
8327
|
return super.init(els, options, _NavigationRail);
|
|
@@ -6194,7 +8351,7 @@ var NavigationRail = class _NavigationRail extends Component {
|
|
|
6194
8351
|
};
|
|
6195
8352
|
|
|
6196
8353
|
// src/ts/components/tabs.ts
|
|
6197
|
-
var
|
|
8354
|
+
var _defaults23 = {
|
|
6198
8355
|
duration: 300,
|
|
6199
8356
|
onShow: null,
|
|
6200
8357
|
swipeable: false,
|
|
@@ -6281,7 +8438,7 @@ var Tabs = class _Tabs extends Component {
|
|
|
6281
8438
|
this._setupEventHandlers();
|
|
6282
8439
|
}
|
|
6283
8440
|
static get defaults() {
|
|
6284
|
-
return
|
|
8441
|
+
return _defaults23;
|
|
6285
8442
|
}
|
|
6286
8443
|
/**
|
|
6287
8444
|
* Initializes instances of Tabs.
|
|
@@ -6468,7 +8625,7 @@ var Tabs = class _Tabs extends Component {
|
|
|
6468
8625
|
};
|
|
6469
8626
|
|
|
6470
8627
|
// src/ts/components/timepicker.ts
|
|
6471
|
-
var
|
|
8628
|
+
var _defaults24 = {
|
|
6472
8629
|
dialRadius: 135,
|
|
6473
8630
|
outerRadius: 105,
|
|
6474
8631
|
innerRadius: 70,
|
|
@@ -6664,7 +8821,7 @@ var _Timepicker = class _Timepicker extends Component {
|
|
|
6664
8821
|
}
|
|
6665
8822
|
}
|
|
6666
8823
|
static get defaults() {
|
|
6667
|
-
return
|
|
8824
|
+
return _defaults24;
|
|
6668
8825
|
}
|
|
6669
8826
|
/**
|
|
6670
8827
|
* Initializes instances of Timepicker.
|
|
@@ -7026,7 +9183,7 @@ _Timepicker._template = `<div class="timepicker-container">
|
|
|
7026
9183
|
var Timepicker = _Timepicker;
|
|
7027
9184
|
|
|
7028
9185
|
// src/ts/components/snackbar.ts
|
|
7029
|
-
var
|
|
9186
|
+
var _defaults25 = {
|
|
7030
9187
|
text: "",
|
|
7031
9188
|
action: "",
|
|
7032
9189
|
onAction: null,
|
|
@@ -7076,7 +9233,7 @@ var _Snackbar = class _Snackbar {
|
|
|
7076
9233
|
this._setTimer();
|
|
7077
9234
|
}
|
|
7078
9235
|
static get defaults() {
|
|
7079
|
-
return
|
|
9236
|
+
return _defaults25;
|
|
7080
9237
|
}
|
|
7081
9238
|
static getInstance(el) {
|
|
7082
9239
|
return el["Expressive_Snackbar"];
|
|
@@ -7373,7 +9530,7 @@ _Snackbar._draggedSnackbar = null;
|
|
|
7373
9530
|
var Snackbar = _Snackbar;
|
|
7374
9531
|
|
|
7375
9532
|
// src/ts/components/tooltip.ts
|
|
7376
|
-
var
|
|
9533
|
+
var _defaults26 = {
|
|
7377
9534
|
exitDelay: 200,
|
|
7378
9535
|
enterDelay: 0,
|
|
7379
9536
|
text: "",
|
|
@@ -7455,7 +9612,7 @@ var Tooltip = class _Tooltip extends Component {
|
|
|
7455
9612
|
this._setupEventHandlers();
|
|
7456
9613
|
}
|
|
7457
9614
|
static get defaults() {
|
|
7458
|
-
return
|
|
9615
|
+
return _defaults26;
|
|
7459
9616
|
}
|
|
7460
9617
|
/**
|
|
7461
9618
|
* Initializes instances of Tooltip.
|
|
@@ -7674,11 +9831,31 @@ var AUTO_INIT_COMPONENTS = {
|
|
|
7674
9831
|
},
|
|
7675
9832
|
Carousel: { component: Carousel, selector: ".carousel" },
|
|
7676
9833
|
Chips: { component: Chips, selector: ".chips" },
|
|
9834
|
+
ColumnChart: { component: ColumnChart, selector: ".column-chart" },
|
|
9835
|
+
BarChart: { component: BarChart, selector: ".bar-chart" },
|
|
9836
|
+
PieChart: { component: PieChart, selector: ".pie-chart" },
|
|
9837
|
+
HeatmapChart: { component: HeatmapChart, selector: ".heatmap-chart" },
|
|
9838
|
+
RadarChart: { component: RadarChart, selector: ".radar-chart" },
|
|
9839
|
+
RadialChart: { component: RadialChart, selector: ".radial-chart" },
|
|
9840
|
+
MixedChart: { component: MixedChart, selector: ".mixed-chart" },
|
|
9841
|
+
CommandPalette: {
|
|
9842
|
+
component: CommandPalette,
|
|
9843
|
+
selector: "dialog.command-palette"
|
|
9844
|
+
},
|
|
9845
|
+
Questionnaire: {
|
|
9846
|
+
component: Questionnaire,
|
|
9847
|
+
selector: "form.questionnaire"
|
|
9848
|
+
},
|
|
7677
9849
|
Datepicker: {
|
|
7678
9850
|
component: Datepicker,
|
|
7679
9851
|
selector: ".datepicker, .date-picker"
|
|
7680
9852
|
},
|
|
9853
|
+
LineChart: { component: LineChart, selector: ".line-chart" },
|
|
7681
9854
|
Menu: { component: Menu, selector: ".menu-trigger" },
|
|
9855
|
+
MessageScroller: {
|
|
9856
|
+
component: MessageScroller,
|
|
9857
|
+
selector: ".message-scroller"
|
|
9858
|
+
},
|
|
7682
9859
|
Lightbox: { component: Lightbox, selector: ".lightboxed" },
|
|
7683
9860
|
ScrollSpy: { component: ScrollSpy, selector: ".scrollspy" },
|
|
7684
9861
|
FormSelect: { component: FormSelect, selector: "select" },
|
|
@@ -7712,7 +9889,7 @@ function AutoInit(context = document.body, options) {
|
|
|
7712
9889
|
}
|
|
7713
9890
|
|
|
7714
9891
|
// src/ts/modular.ts
|
|
7715
|
-
var version = "0.
|
|
9892
|
+
var version = "0.13.1";
|
|
7716
9893
|
|
|
7717
9894
|
// src/ts/index.ts
|
|
7718
9895
|
Forms.Init();
|