@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.
Files changed (171) hide show
  1. package/CHANGELOG.md +231 -1
  2. package/README.md +37 -1
  3. package/dist/css/expressive.css +5111 -36
  4. package/dist/css/expressive.css.map +1 -1
  5. package/dist/css/expressive.min.css +1 -1
  6. package/dist/css/expressive.min.css.map +1 -1
  7. package/dist/fonts/material-symbols-outlined.woff2 +0 -0
  8. package/dist/fonts/material-symbols-rounded.woff2 +0 -0
  9. package/dist/fonts/material-symbols-sharp.woff2 +0 -0
  10. package/dist/js/chunks/{chunk-HLDXZ7XB.mjs → chunk-3CLXLPYS.mjs} +2 -2
  11. package/dist/js/chunks/chunk-3DHK5CZU.mjs +105 -0
  12. package/dist/js/chunks/chunk-3DHK5CZU.mjs.map +7 -0
  13. package/dist/js/chunks/chunk-4PWAPV6F.mjs +7 -0
  14. package/dist/js/chunks/chunk-4PWAPV6F.mjs.map +7 -0
  15. package/dist/js/chunks/{chunk-HB6IW7QQ.mjs → chunk-55IA2XUC.mjs} +2 -2
  16. package/dist/js/chunks/chunk-CCWDV5CR.mjs +292 -0
  17. package/dist/js/chunks/chunk-CCWDV5CR.mjs.map +7 -0
  18. package/dist/js/chunks/chunk-DTKR54I6.mjs +270 -0
  19. package/dist/js/chunks/chunk-DTKR54I6.mjs.map +7 -0
  20. package/dist/js/chunks/chunk-GVGT6MIU.mjs +47 -0
  21. package/dist/js/chunks/chunk-GVGT6MIU.mjs.map +7 -0
  22. package/dist/js/chunks/chunk-HKT2S7VM.mjs +48 -0
  23. package/dist/js/chunks/chunk-HKT2S7VM.mjs.map +7 -0
  24. package/dist/js/chunks/chunk-K4CJCRGE.mjs +262 -0
  25. package/dist/js/chunks/chunk-K4CJCRGE.mjs.map +7 -0
  26. package/dist/js/chunks/chunk-LNCCYGPC.mjs +163 -0
  27. package/dist/js/chunks/chunk-LNCCYGPC.mjs.map +7 -0
  28. package/dist/js/chunks/chunk-NQLC3YMW.mjs +1 -0
  29. package/dist/js/chunks/chunk-O5XPWXIQ.mjs +406 -0
  30. package/dist/js/chunks/chunk-O5XPWXIQ.mjs.map +7 -0
  31. package/dist/js/chunks/chunk-P3G3QHII.mjs +166 -0
  32. package/dist/js/chunks/chunk-P3G3QHII.mjs.map +7 -0
  33. package/dist/js/chunks/{chunk-H2WA4XHH.mjs → chunk-PLYXLSH3.mjs} +4 -4
  34. package/dist/js/chunks/chunk-QKWPQK5N.mjs +179 -0
  35. package/dist/js/chunks/chunk-QKWPQK5N.mjs.map +7 -0
  36. package/dist/js/chunks/chunk-RHRQKTTX.mjs +139 -0
  37. package/dist/js/chunks/chunk-RHRQKTTX.mjs.map +7 -0
  38. package/dist/js/chunks/chunk-SPETGK25.mjs +59 -0
  39. package/dist/js/chunks/chunk-SPETGK25.mjs.map +7 -0
  40. package/dist/js/chunks/{chunk-YSBCHYXP.mjs → chunk-U2OTHOTI.mjs} +116 -12
  41. package/dist/js/chunks/chunk-U2OTHOTI.mjs.map +7 -0
  42. package/dist/js/chunks/chunk-VTLBW7QS.mjs +124 -0
  43. package/dist/js/chunks/chunk-VTLBW7QS.mjs.map +7 -0
  44. package/dist/js/chunks/{chunk-RFZZCYVU.mjs → chunk-XGHORT7J.mjs} +2 -2
  45. package/dist/js/chunks/{chunk-HNFSMIBY.mjs → chunk-XH6BEEMY.mjs} +69 -16
  46. package/dist/js/chunks/chunk-XH6BEEMY.mjs.map +7 -0
  47. package/dist/js/expressive.cjs +2209 -32
  48. package/dist/js/expressive.cjs.map +4 -4
  49. package/dist/js/expressive.js +2209 -32
  50. package/dist/js/expressive.js.map +4 -4
  51. package/dist/js/expressive.min.js +10 -10
  52. package/dist/js/expressive.min.js.map +4 -4
  53. package/dist/js/expressive.mjs +68 -20
  54. package/dist/js/expressive.mjs.map +1 -1
  55. package/dist/js/internal/behaviors/forms.mjs +1 -1
  56. package/dist/js/internal/components/autocomplete.mjs +2 -2
  57. package/dist/js/internal/components/barChart.mjs +10 -0
  58. package/dist/js/internal/components/barChart.mjs.map +7 -0
  59. package/dist/js/internal/components/chips.mjs +3 -3
  60. package/dist/js/internal/components/columnChart.mjs +9 -0
  61. package/dist/js/internal/components/columnChart.mjs.map +7 -0
  62. package/dist/js/internal/components/commandPalette.mjs +9 -0
  63. package/dist/js/internal/components/commandPalette.mjs.map +7 -0
  64. package/dist/js/internal/components/datepicker.mjs +3 -3
  65. package/dist/js/internal/components/heatmapChart.mjs +9 -0
  66. package/dist/js/internal/components/heatmapChart.mjs.map +7 -0
  67. package/dist/js/internal/components/index.mjs +65 -17
  68. package/dist/js/internal/components/lineChart.mjs +12 -0
  69. package/dist/js/internal/components/lineChart.mjs.map +7 -0
  70. package/dist/js/internal/components/menu.mjs +1 -1
  71. package/dist/js/internal/components/messageScroller.mjs +9 -0
  72. package/dist/js/internal/components/messageScroller.mjs.map +7 -0
  73. package/dist/js/internal/components/mixedChart.mjs +10 -0
  74. package/dist/js/internal/components/mixedChart.mjs.map +7 -0
  75. package/dist/js/internal/components/pieChart.mjs +9 -0
  76. package/dist/js/internal/components/pieChart.mjs.map +7 -0
  77. package/dist/js/internal/components/questionnaire.mjs +9 -0
  78. package/dist/js/internal/components/questionnaire.mjs.map +7 -0
  79. package/dist/js/internal/components/radarChart.mjs +9 -0
  80. package/dist/js/internal/components/radarChart.mjs.map +7 -0
  81. package/dist/js/internal/components/radialChart.mjs +9 -0
  82. package/dist/js/internal/components/radialChart.mjs.map +7 -0
  83. package/dist/js/internal/components/registry.mjs +23 -11
  84. package/dist/js/internal/components/richTextEditor.mjs +9 -0
  85. package/dist/js/internal/components/richTextEditor.mjs.map +7 -0
  86. package/dist/js/internal/components/select.mjs +2 -2
  87. package/dist/js/modular.mjs +68 -20
  88. package/dist/types/behaviors/forms.d.ts +22 -0
  89. package/dist/types/components/barChart.d.ts +24 -0
  90. package/dist/types/components/columnChart.d.ts +21 -0
  91. package/dist/types/components/commandPalette.d.ts +96 -0
  92. package/dist/types/components/heatmapChart.d.ts +49 -0
  93. package/dist/types/components/index.d.ts +12 -0
  94. package/dist/types/components/lineChart.d.ts +134 -0
  95. package/dist/types/components/menu.d.ts +31 -0
  96. package/dist/types/components/messageScroller.d.ts +111 -0
  97. package/dist/types/components/mixedChart.d.ts +25 -0
  98. package/dist/types/components/pieChart.d.ts +40 -0
  99. package/dist/types/components/questionnaire.d.ts +96 -0
  100. package/dist/types/components/radarChart.d.ts +39 -0
  101. package/dist/types/components/radialChart.d.ts +45 -0
  102. package/dist/types/components/registry.d.ts +44 -0
  103. package/dist/types/components/richTextEditor.d.ts +71 -0
  104. package/dist/types/modular.d.ts +10 -1
  105. package/package.json +4 -1
  106. package/src/sass/README.md +3 -6
  107. package/src/sass/abstracts/_index.scss +1 -1
  108. package/src/sass/abstracts/_mixins.scss +226 -0
  109. package/src/sass/abstracts/_variables.scss +14 -0
  110. package/src/sass/base/_typography.scss +45 -0
  111. package/src/sass/components/_accordion.scss +161 -0
  112. package/src/sass/components/_aura.scss +119 -0
  113. package/src/sass/components/_avatar.scss +86 -0
  114. package/src/sass/components/_bar-chart.scss +285 -0
  115. package/src/sass/components/_bottom-sheet.scss +1 -0
  116. package/src/sass/components/_button-groups.scss +13 -3
  117. package/src/sass/components/_cards.scss +16 -13
  118. package/src/sass/components/_column-chart.scss +134 -0
  119. package/src/sass/components/_command-palette.scss +140 -0
  120. package/src/sass/components/_countdown.scss +89 -0
  121. package/src/sass/components/_data-table.scss +188 -0
  122. package/src/sass/components/_dialog.scss +1 -0
  123. package/src/sass/components/_empty-state.scss +78 -0
  124. package/src/sass/components/_expanding-card.scss +3 -0
  125. package/src/sass/components/_heatmap-chart.scss +156 -0
  126. package/src/sass/components/_index.scss +27 -0
  127. package/src/sass/components/_kpi.scss +162 -0
  128. package/src/sass/components/_line-chart.scss +102 -0
  129. package/src/sass/components/_marquee.scss +111 -0
  130. package/src/sass/components/_menu.scss +10 -0
  131. package/src/sass/components/_message-scroller.scss +66 -0
  132. package/src/sass/components/_message.scss +202 -0
  133. package/src/sass/components/_mixed-chart.scss +157 -0
  134. package/src/sass/components/_navigation-bar.scss +1 -0
  135. package/src/sass/components/_navigation-rail.scss +9 -0
  136. package/src/sass/components/_pie-chart.scss +203 -0
  137. package/src/sass/components/_popover.scss +112 -0
  138. package/src/sass/components/_questionnaire.scss +144 -0
  139. package/src/sass/components/_radar-chart.scss +205 -0
  140. package/src/sass/components/_radial-chart.scss +174 -0
  141. package/src/sass/components/_rich-text-editor.scss +99 -0
  142. package/src/sass/components/_search.scss +1 -0
  143. package/src/sass/components/_side-sheet.scss +1 -0
  144. package/src/sass/components/_skeleton.scss +81 -0
  145. package/src/sass/components/_snackbar.scss +1 -0
  146. package/src/sass/components/_stat.scss +86 -0
  147. package/src/sass/components/_stepper.scss +190 -0
  148. package/src/sass/components/_timeline.scss +114 -0
  149. package/src/sass/components/_toolbar.scss +1 -0
  150. package/src/sass/components/_tooltip.scss +2 -0
  151. package/src/sass/components/_tree.scss +139 -0
  152. package/src/sass/components/forms/_drop-zone.scss +128 -0
  153. package/src/sass/components/forms/_index.scss +2 -0
  154. package/src/sass/components/forms/_input-fields.scss +133 -2
  155. package/src/sass/components/forms/_rating.scss +165 -0
  156. package/src/sass/tokens/_reference.scss +3 -15
  157. package/src/sass/utilities/_index.scss +2 -0
  158. package/src/sass/utilities/_scroll-area.scss +35 -0
  159. package/src/sass/utilities/_shapes.scss +164 -0
  160. package/dist/js/chunks/chunk-G3I5H2U3.mjs +0 -7
  161. package/dist/js/chunks/chunk-G3I5H2U3.mjs.map +0 -7
  162. package/dist/js/chunks/chunk-HNFSMIBY.mjs.map +0 -7
  163. package/dist/js/chunks/chunk-PSSHQWCX.mjs +0 -100
  164. package/dist/js/chunks/chunk-PSSHQWCX.mjs.map +0 -7
  165. package/dist/js/chunks/chunk-WWPAPHMA.mjs +0 -1
  166. package/dist/js/chunks/chunk-YSBCHYXP.mjs.map +0 -7
  167. /package/dist/js/chunks/{chunk-HLDXZ7XB.mjs.map → chunk-3CLXLPYS.mjs.map} +0 -0
  168. /package/dist/js/chunks/{chunk-HB6IW7QQ.mjs.map → chunk-55IA2XUC.mjs.map} +0 -0
  169. /package/dist/js/chunks/{chunk-WWPAPHMA.mjs.map → chunk-NQLC3YMW.mjs.map} +0 -0
  170. /package/dist/js/chunks/{chunk-H2WA4XHH.mjs.map → chunk-PLYXLSH3.mjs.map} +0 -0
  171. /package/dist/js/chunks/{chunk-RFZZCYVU.mjs.map → chunk-XGHORT7J.mjs.map} +0 -0
@@ -0,0 +1,292 @@
1
+ import {
2
+ Utils
3
+ } from "./chunk-6OMRGML6.mjs";
4
+ import {
5
+ Component
6
+ } from "./chunk-LTWPGFBR.mjs";
7
+
8
+ // src/ts/components/messageScroller.ts
9
+ var _defaults = {
10
+ autoScroll: true,
11
+ defaultScrollPosition: "end",
12
+ peek: 64
13
+ };
14
+ var EDGE = 2;
15
+ var UP_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "PageUp", "Home"]);
16
+ var RELEASE_EVENTS = ["pointerup", "pointercancel", "touchend", "touchcancel"];
17
+ var MessageScroller = class _MessageScroller extends Component {
18
+ constructor(el, options) {
19
+ super(el, options, _MessageScroller);
20
+ this._opened = false;
21
+ this._following = false;
22
+ /** A press is down: it may be a tap or a scrollbar drag. */
23
+ this._holding = false;
24
+ this._lastTop = 0;
25
+ /** Where a programmatic scroll is heading; null when none is running. */
26
+ this._target = null;
27
+ /** Distance left to the target at the last scroll event. */
28
+ this._distance = Infinity;
29
+ /** The first visible message and its offset from the viewport top. */
30
+ this._ref = null;
31
+ this._refOffset = 0;
32
+ /** The latest anchored turn, which the spacer lets reach the top. */
33
+ this._anchor = null;
34
+ this._spacer = 0;
35
+ this._last = null;
36
+ this._resizeObserver = null;
37
+ this._mutationObserver = null;
38
+ // A programmatic scroll ends when it arrives, or when a scroll moves away
39
+ // from its target because something else took over.
40
+ this._handleScroll = () => {
41
+ if (this._target !== null) {
42
+ const distance = Math.abs(this.viewport.scrollTop - this._resolve(this._target));
43
+ if (distance > EDGE && distance < this._distance) {
44
+ this._distance = distance;
45
+ return;
46
+ }
47
+ this._target = null;
48
+ }
49
+ this._settle();
50
+ };
51
+ this._handleUserScroll = (e) => {
52
+ if (e.type === "wheel" && e.deltaY >= 0) return;
53
+ if (e.type === "keydown" && !UP_KEYS.has(e.key)) return;
54
+ this._target = null;
55
+ if (e.type === "pointerdown" || e.type === "touchstart") this._holding = true;
56
+ else this._following = false;
57
+ };
58
+ this._handleRelease = () => {
59
+ if (!this._holding) return;
60
+ this._holding = false;
61
+ if (this._following) this.viewport.scrollTop = this.viewport.scrollHeight;
62
+ this._sync();
63
+ };
64
+ this._handleButtonClick = () => this.scrollToEnd();
65
+ this._handleMutations = (records) => {
66
+ for (const record of records) {
67
+ record.addedNodes.forEach((node) => {
68
+ if (node instanceof Element) this._resizeObserver?.observe(node);
69
+ });
70
+ record.removedNodes.forEach((node) => {
71
+ if (node instanceof Element) this._resizeObserver?.unobserve(node);
72
+ });
73
+ }
74
+ const previous = this._last;
75
+ this._last = this.viewport.lastElementChild;
76
+ if (!this._opened) return;
77
+ if (!this._following && this._target === null) this._restore();
78
+ if (!this.options.autoScroll) return;
79
+ if (previous && previous.parentElement !== this.viewport) return;
80
+ for (let item = this._last; item && item !== previous; item = item.previousElementSibling) {
81
+ if (item.hasAttribute("data-scroll-anchor")) {
82
+ this._anchor = item;
83
+ this._updateSpacer();
84
+ this._following = true;
85
+ this._scrollTo(this._top(item) - this.options.peek);
86
+ return;
87
+ }
88
+ }
89
+ };
90
+ this._handleResize = () => {
91
+ if (!this._opened) {
92
+ if (this.viewport.clientHeight > 0) this._open();
93
+ return;
94
+ }
95
+ this._updateSpacer();
96
+ const v = this.viewport;
97
+ if (this._target === "end") this._scrollTo("end");
98
+ else if (this._target === null) {
99
+ if (this._following && !this._holding) v.scrollTop = v.scrollHeight;
100
+ else this._restore();
101
+ }
102
+ this._sync();
103
+ };
104
+ this.el["Expressive_MessageScroller"] = this;
105
+ this.options = {
106
+ ..._MessageScroller.defaults,
107
+ ...options
108
+ };
109
+ this.viewport = el.querySelector(":scope > .message-scroller-viewport");
110
+ this.button = el.querySelector(":scope > .message-scroller-button");
111
+ if (!this.viewport) {
112
+ console.error("MessageScroller needs a .message-scroller-viewport child.");
113
+ return;
114
+ }
115
+ this._last = this.viewport.lastElementChild;
116
+ if (this.viewport.clientHeight > 0) this._open();
117
+ if (typeof ResizeObserver !== "undefined") {
118
+ this._resizeObserver = new ResizeObserver(this._handleResize);
119
+ this._resizeObserver.observe(this.viewport, { box: "border-box" });
120
+ for (const item of Array.from(this.viewport.children)) this._resizeObserver.observe(item);
121
+ }
122
+ this._mutationObserver = new MutationObserver(this._handleMutations);
123
+ this._mutationObserver.observe(this.viewport, { childList: true });
124
+ this.viewport.addEventListener("scroll", this._handleScroll, { passive: true });
125
+ for (const type of ["wheel", "touchstart", "pointerdown", "keydown"]) {
126
+ this.viewport.addEventListener(type, this._handleUserScroll, { passive: true });
127
+ }
128
+ for (const type of RELEASE_EVENTS) document.addEventListener(type, this._handleRelease);
129
+ this.button?.addEventListener("click", this._handleButtonClick);
130
+ }
131
+ static get defaults() {
132
+ return _defaults;
133
+ }
134
+ /**
135
+ * Initializes instances of MessageScroller.
136
+ * @param els HTML elements.
137
+ * @param options Component options.
138
+ */
139
+ static init(els, options = {}) {
140
+ return super.init(els, options, _MessageScroller);
141
+ }
142
+ static getInstance(el) {
143
+ return el["Expressive_MessageScroller"];
144
+ }
145
+ destroy() {
146
+ this.el["Expressive_MessageScroller"] = void 0;
147
+ if (!this.viewport) return;
148
+ this._resizeObserver?.disconnect();
149
+ this._mutationObserver?.disconnect();
150
+ this.viewport.removeEventListener("scroll", this._handleScroll);
151
+ for (const type of ["wheel", "touchstart", "pointerdown", "keydown"]) {
152
+ this.viewport.removeEventListener(type, this._handleUserScroll);
153
+ }
154
+ for (const type of RELEASE_EVENTS) document.removeEventListener(type, this._handleRelease);
155
+ this.viewport.style.removeProperty("--md-comp-message-scroller-spacer");
156
+ if (this.button) {
157
+ this.button.removeEventListener("click", this._handleButtonClick);
158
+ this.button.inert = false;
159
+ }
160
+ }
161
+ /** Scrolls to the latest message and resumes following. */
162
+ scrollToEnd() {
163
+ this._following = this.options.autoScroll;
164
+ this._scrollTo("end");
165
+ }
166
+ /** Scrolls to the first message. */
167
+ scrollToStart() {
168
+ this._following = false;
169
+ this._scrollTo(0);
170
+ }
171
+ /**
172
+ * Scrolls the element with this id to the top of the viewport, below the
173
+ * peek, and stops following.
174
+ * @returns false when no such element is inside the viewport.
175
+ */
176
+ scrollToMessage(id) {
177
+ const target = Utils.getElementById(this.viewport, id);
178
+ if (!target || !this.viewport.contains(target)) return false;
179
+ this._following = false;
180
+ this._scrollTo(this._top(target) - this.options.peek);
181
+ return true;
182
+ }
183
+ _open() {
184
+ this._opened = true;
185
+ const v = this.viewport;
186
+ const position = this.options.defaultScrollPosition;
187
+ const anchors = v.querySelectorAll(":scope > [data-scroll-anchor]");
188
+ const anchor = position === "last-anchor" ? anchors[anchors.length - 1] : null;
189
+ if (position === "start") v.scrollTop = 0;
190
+ else if (anchor && this._contentEnd() - this._top(anchor) + this.options.peek > v.clientHeight) {
191
+ v.scrollTop = this._top(anchor) - this.options.peek;
192
+ } else v.scrollTop = v.scrollHeight;
193
+ this._settle();
194
+ }
195
+ /**
196
+ * Records where the reader is once no programmatic scroll is running.
197
+ * Content can grow between a scroll and the resize that re-pins the end,
198
+ * so only moving toward the start releases following.
199
+ */
200
+ _settle() {
201
+ const top = this.viewport.scrollTop;
202
+ const kept = this._following && top >= this._lastTop - EDGE;
203
+ this._following = this.options.autoScroll && (kept || this._atEnd());
204
+ this._lastTop = top;
205
+ this._capture();
206
+ this._sync();
207
+ }
208
+ _scrollTo(target) {
209
+ const v = this.viewport;
210
+ if (Math.abs(v.scrollTop - this._resolve(target)) <= EDGE) {
211
+ this._target = null;
212
+ this._settle();
213
+ return;
214
+ }
215
+ this._target = target;
216
+ this._distance = Infinity;
217
+ const reduce = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
218
+ v.scrollTo({
219
+ top: target === "end" ? v.scrollHeight : target,
220
+ behavior: reduce ? "auto" : "smooth"
221
+ });
222
+ }
223
+ _resolve(target) {
224
+ const max = this.viewport.scrollHeight - this.viewport.clientHeight;
225
+ return target === "end" ? max : Math.max(0, Math.min(target, max));
226
+ }
227
+ _atEnd() {
228
+ const v = this.viewport;
229
+ return v.scrollHeight - v.scrollTop - v.clientHeight <= EDGE;
230
+ }
231
+ /** Offset of an element from the top of the scrolled content. */
232
+ _top(el) {
233
+ const v = this.viewport;
234
+ return el.getBoundingClientRect().top - v.getBoundingClientRect().top - v.clientTop + v.scrollTop;
235
+ }
236
+ /** Bottom of the content without the spacer, padding included. */
237
+ _contentEnd() {
238
+ const last = this.viewport.lastElementChild;
239
+ if (!last) return 0;
240
+ const padding = parseFloat(getComputedStyle(this.viewport).paddingBlockEnd) - this._spacer;
241
+ return this._top(last) + last.getBoundingClientRect().height + padding;
242
+ }
243
+ /** Remembers the first visible message so prepends can keep it in place. */
244
+ _capture() {
245
+ const items = this.viewport.children;
246
+ const top = this.viewport.getBoundingClientRect().top + this.viewport.clientTop;
247
+ let low = 0;
248
+ let high = items.length - 1;
249
+ while (low < high) {
250
+ const mid = low + high >> 1;
251
+ if (items[mid].getBoundingClientRect().bottom > top) high = mid;
252
+ else low = mid + 1;
253
+ }
254
+ this._ref = items[low] ?? null;
255
+ this._refOffset = this._ref ? this._ref.getBoundingClientRect().top - top : 0;
256
+ }
257
+ /** Moves the remembered message back to where the reader last saw it. */
258
+ _restore() {
259
+ if (this._ref?.parentElement !== this.viewport) {
260
+ this._capture();
261
+ return;
262
+ }
263
+ const top = this.viewport.getBoundingClientRect().top + this.viewport.clientTop;
264
+ const delta = this._ref.getBoundingClientRect().top - top - this._refOffset;
265
+ if (Math.abs(delta) >= 1) this.viewport.scrollTop += delta;
266
+ }
267
+ /** Sizes the blank space that lets the latest anchored turn reach the top. */
268
+ _updateSpacer() {
269
+ let space = 0;
270
+ if (this._anchor?.parentElement === this.viewport) {
271
+ const room = this._top(this._anchor) - this.options.peek + this.viewport.clientHeight;
272
+ space = Math.max(0, Math.ceil(room - this._contentEnd()));
273
+ } else this._anchor = null;
274
+ if (space === this._spacer) return;
275
+ this._spacer = space;
276
+ if (space) this.viewport.style.setProperty("--md-comp-message-scroller-spacer", `${space}px`);
277
+ else this.viewport.style.removeProperty("--md-comp-message-scroller-spacer");
278
+ }
279
+ /** The button is inert while there is nothing below. */
280
+ _sync() {
281
+ if (!this.button) return;
282
+ const idle = this._atEnd();
283
+ const root = this.button.getRootNode();
284
+ if (idle && root.activeElement === this.button) this.viewport.focus({ preventScroll: true });
285
+ this.button.inert = idle;
286
+ }
287
+ };
288
+
289
+ export {
290
+ MessageScroller
291
+ };
292
+ //# sourceMappingURL=chunk-CCWDV5CR.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/ts/components/messageScroller.ts"],
4
+ "sourcesContent": ["import { Utils } from '../core/utils';\nimport { Component, BaseOptions, InitElements, InitElement } from '../core/component';\n\nexport interface MessageScrollerOptions extends BaseOptions {\n /**\n * If true, the viewport follows new content while the reader is at the\n * end, and a newly appended scroll anchor is brought to the top.\n * @default true\n */\n autoScroll: boolean;\n /**\n * Where the transcript opens: `'end'`, `'start'`, or `'last-anchor'`, the\n * start of the last anchored turn. `'last-anchor'` opens at the end when\n * that turn already fits in the viewport.\n * @default 'end'\n */\n defaultScrollPosition: 'start' | 'end' | 'last-anchor';\n /**\n * Pixels of the previous message left visible above an anchored turn or a\n * message reached with `scrollToMessage()`.\n * @default 64\n */\n peek: number;\n}\n\nconst _defaults: MessageScrollerOptions = {\n autoScroll: true,\n defaultScrollPosition: 'end',\n peek: 64\n};\n\n// Distance from a scroll target, in px, that still counts as reaching it.\n// Fractional layouts rarely land on the exact pixel.\nconst EDGE = 2;\n\n// Keys that scroll toward the start. They release following at once, before\n// the next streamed chunk can pull the viewport back to the end.\nconst UP_KEYS = new Set(['ArrowUp', 'PageUp', 'Home']);\n\n// Listened on the document: a scrollbar drag can end outside the viewport.\nconst RELEASE_EVENTS = ['pointerup', 'pointercancel', 'touchend', 'touchcancel'];\n\ntype Target = number | 'end';\n\n/**\n * A chat scroll container: a `.message-scroller` frame holding a scrolling\n * `.message-scroller-viewport` of messages and an optional\n * `.message-scroller-button`. Every child of the viewport is one message.\n *\n * While the reader is at the end, the viewport follows streamed output.\n * Scrolling toward the start releases it; reaching the end again, or the\n * button, resumes it. An appended message marked `data-scroll-anchor` starts a\n * turn and is scrolled to the top, with blank space below it until the reply\n * fills the viewport. Messages added above keep the reader's place.\n */\nexport class MessageScroller extends Component<MessageScrollerOptions> {\n /** The scrolling element. */\n viewport: HTMLElement;\n /** The scroll-to-latest button, if present. */\n button: HTMLButtonElement | null;\n private _opened = false;\n private _following = false;\n /** A press is down: it may be a tap or a scrollbar drag. */\n private _holding = false;\n private _lastTop = 0;\n /** Where a programmatic scroll is heading; null when none is running. */\n private _target: Target | null = null;\n /** Distance left to the target at the last scroll event. */\n private _distance = Infinity;\n /** The first visible message and its offset from the viewport top. */\n private _ref: Element | null = null;\n private _refOffset = 0;\n /** The latest anchored turn, which the spacer lets reach the top. */\n private _anchor: Element | null = null;\n private _spacer = 0;\n private _last: Element | null = null;\n private _resizeObserver: ResizeObserver | null = null;\n private _mutationObserver: MutationObserver | null = null;\n\n constructor(el: HTMLElement, options: Partial<MessageScrollerOptions>) {\n super(el, options, MessageScroller);\n this.el['Expressive_MessageScroller'] = this;\n this.options = {\n ...MessageScroller.defaults,\n ...options\n };\n this.viewport = el.querySelector(':scope > .message-scroller-viewport');\n this.button = el.querySelector(':scope > .message-scroller-button');\n if (!this.viewport) {\n console.error('MessageScroller needs a .message-scroller-viewport child.');\n return;\n }\n this._last = this.viewport.lastElementChild;\n // A viewport that is not rendered yet (a closed dialog, a hidden tab)\n // has no size to position against; it opens on its first resize.\n if (this.viewport.clientHeight > 0) this._open();\n if (typeof ResizeObserver !== 'undefined') {\n this._resizeObserver = new ResizeObserver(this._handleResize);\n // Border box: the spacer changes the padding, which must not count.\n this._resizeObserver.observe(this.viewport, { box: 'border-box' });\n for (const item of Array.from(this.viewport.children)) this._resizeObserver.observe(item);\n }\n this._mutationObserver = new MutationObserver(this._handleMutations);\n this._mutationObserver.observe(this.viewport, { childList: true });\n this.viewport.addEventListener('scroll', this._handleScroll, { passive: true });\n for (const type of ['wheel', 'touchstart', 'pointerdown', 'keydown']) {\n this.viewport.addEventListener(type, this._handleUserScroll, { passive: true });\n }\n for (const type of RELEASE_EVENTS) document.addEventListener(type, this._handleRelease);\n this.button?.addEventListener('click', this._handleButtonClick);\n }\n\n static get defaults(): MessageScrollerOptions {\n return _defaults;\n }\n\n /**\n * Initializes instance of MessageScroller.\n * @param el HTML element.\n * @param options Component options.\n */\n static init(el: HTMLElement, options?: Partial<MessageScrollerOptions>): MessageScroller;\n /**\n * Initializes instances of MessageScroller.\n * @param els HTML elements.\n * @param options Component options.\n */\n static init(\n els: InitElements<InitElement>,\n options?: Partial<MessageScrollerOptions>\n ): MessageScroller[];\n /**\n * Initializes instances of MessageScroller.\n * @param els HTML elements.\n * @param options Component options.\n */\n static init(\n els: HTMLElement | InitElements<InitElement>,\n options: Partial<MessageScrollerOptions> = {}\n ): MessageScroller | MessageScroller[] {\n return super.init(els, options, MessageScroller);\n }\n\n static getInstance(el: HTMLElement): MessageScroller {\n return el['Expressive_MessageScroller'];\n }\n\n destroy() {\n this.el['Expressive_MessageScroller'] = undefined;\n if (!this.viewport) return;\n this._resizeObserver?.disconnect();\n this._mutationObserver?.disconnect();\n this.viewport.removeEventListener('scroll', this._handleScroll);\n for (const type of ['wheel', 'touchstart', 'pointerdown', 'keydown']) {\n this.viewport.removeEventListener(type, this._handleUserScroll);\n }\n for (const type of RELEASE_EVENTS) document.removeEventListener(type, this._handleRelease);\n this.viewport.style.removeProperty('--md-comp-message-scroller-spacer');\n if (this.button) {\n this.button.removeEventListener('click', this._handleButtonClick);\n this.button.inert = false;\n }\n }\n\n /** Scrolls to the latest message and resumes following. */\n scrollToEnd() {\n // Set now: output streaming faster than the scroll can end it early.\n this._following = this.options.autoScroll;\n this._scrollTo('end');\n }\n\n /** Scrolls to the first message. */\n scrollToStart() {\n this._following = false;\n this._scrollTo(0);\n }\n\n /**\n * Scrolls the element with this id to the top of the viewport, below the\n * peek, and stops following.\n * @returns false when no such element is inside the viewport.\n */\n scrollToMessage(id: string): boolean {\n const target = Utils.getElementById(this.viewport, id);\n if (!target || !this.viewport.contains(target)) return false;\n this._following = false;\n this._scrollTo(this._top(target) - this.options.peek);\n return true;\n }\n\n private _open() {\n this._opened = true;\n const v = this.viewport;\n const position = this.options.defaultScrollPosition;\n const anchors = v.querySelectorAll(':scope > [data-scroll-anchor]');\n const anchor = position === 'last-anchor' ? anchors[anchors.length - 1] : null;\n if (position === 'start') v.scrollTop = 0;\n else if (anchor && this._contentEnd() - this._top(anchor) + this.options.peek > v.clientHeight) {\n v.scrollTop = this._top(anchor) - this.options.peek;\n } else v.scrollTop = v.scrollHeight;\n this._settle();\n }\n\n /**\n * Records where the reader is once no programmatic scroll is running.\n * Content can grow between a scroll and the resize that re-pins the end,\n * so only moving toward the start releases following.\n */\n private _settle() {\n const top = this.viewport.scrollTop;\n const kept = this._following && top >= this._lastTop - EDGE;\n this._following = this.options.autoScroll && (kept || this._atEnd());\n this._lastTop = top;\n this._capture();\n this._sync();\n }\n\n // A programmatic scroll ends when it arrives, or when a scroll moves away\n // from its target because something else took over.\n private _handleScroll = () => {\n if (this._target !== null) {\n const distance = Math.abs(this.viewport.scrollTop - this._resolve(this._target));\n if (distance > EDGE && distance < this._distance) {\n this._distance = distance;\n return;\n }\n this._target = null;\n }\n this._settle();\n };\n\n private _handleUserScroll = (e: Event) => {\n if (e.type === 'wheel' && (e as WheelEvent).deltaY >= 0) return;\n if (e.type === 'keydown' && !UP_KEYS.has((e as KeyboardEvent).key)) return;\n this._target = null;\n // A press may be a tap on a message or the start of a drag. Hold the end\n // until it is released; a scroll toward the start releases following.\n if (e.type === 'pointerdown' || e.type === 'touchstart') this._holding = true;\n else this._following = false;\n };\n\n private _handleRelease = () => {\n if (!this._holding) return;\n this._holding = false;\n if (this._following) this.viewport.scrollTop = this.viewport.scrollHeight;\n this._sync();\n };\n\n private _handleButtonClick = () => this.scrollToEnd();\n\n private _handleMutations = (records: MutationRecord[]) => {\n for (const record of records) {\n record.addedNodes.forEach((node) => {\n if (node instanceof Element) this._resizeObserver?.observe(node);\n });\n record.removedNodes.forEach((node) => {\n if (node instanceof Element) this._resizeObserver?.unobserve(node);\n });\n }\n const previous = this._last;\n this._last = this.viewport.lastElementChild;\n if (!this._opened) return;\n // Restore here, not only on resize: this runs before any scroll event\n // that could record the shifted position as the reader's place.\n if (!this._following && this._target === null) this._restore();\n if (!this.options.autoScroll) return;\n // Only messages appended after the previous last one start a new turn;\n // anchors in prepended history do not. If the last one was removed, the\n // list was replaced and nothing counts as appended.\n if (previous && previous.parentElement !== this.viewport) return;\n for (let item = this._last; item && item !== previous; item = item.previousElementSibling) {\n if (item.hasAttribute('data-scroll-anchor')) {\n this._anchor = item;\n this._updateSpacer();\n // Follow the reply once the turn reaches the top, unless the reader\n // scrolls away first. _settle keeps it because the scroll went down.\n this._following = true;\n this._scrollTo(this._top(item) - this.options.peek);\n return;\n }\n }\n };\n\n private _handleResize = () => {\n if (!this._opened) {\n if (this.viewport.clientHeight > 0) this._open();\n return;\n }\n this._updateSpacer();\n const v = this.viewport;\n if (this._target === 'end') this._scrollTo('end');\n else if (this._target === null) {\n if (this._following && !this._holding) v.scrollTop = v.scrollHeight;\n else this._restore();\n }\n this._sync();\n };\n\n private _scrollTo(target: Target) {\n const v = this.viewport;\n if (Math.abs(v.scrollTop - this._resolve(target)) <= EDGE) {\n this._target = null;\n this._settle();\n return;\n }\n this._target = target;\n this._distance = Infinity;\n const reduce = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;\n v.scrollTo({\n top: target === 'end' ? v.scrollHeight : target,\n behavior: reduce ? 'auto' : 'smooth'\n });\n }\n\n private _resolve(target: Target) {\n const max = this.viewport.scrollHeight - this.viewport.clientHeight;\n return target === 'end' ? max : Math.max(0, Math.min(target, max));\n }\n\n private _atEnd() {\n const v = this.viewport;\n return v.scrollHeight - v.scrollTop - v.clientHeight <= EDGE;\n }\n\n /** Offset of an element from the top of the scrolled content. */\n private _top(el: Element) {\n const v = this.viewport;\n return el.getBoundingClientRect().top - v.getBoundingClientRect().top - v.clientTop + v.scrollTop;\n }\n\n /** Bottom of the content without the spacer, padding included. */\n private _contentEnd() {\n const last = this.viewport.lastElementChild;\n if (!last) return 0;\n const padding = parseFloat(getComputedStyle(this.viewport).paddingBlockEnd) - this._spacer;\n return this._top(last) + last.getBoundingClientRect().height + padding;\n }\n\n /** Remembers the first visible message so prepends can keep it in place. */\n private _capture() {\n const items = this.viewport.children;\n const top = this.viewport.getBoundingClientRect().top + this.viewport.clientTop;\n let low = 0;\n let high = items.length - 1;\n while (low < high) {\n const mid = (low + high) >> 1;\n if (items[mid].getBoundingClientRect().bottom > top) high = mid;\n else low = mid + 1;\n }\n this._ref = items[low] ?? null;\n this._refOffset = this._ref ? this._ref.getBoundingClientRect().top - top : 0;\n }\n\n /** Moves the remembered message back to where the reader last saw it. */\n private _restore() {\n if (this._ref?.parentElement !== this.viewport) {\n this._capture();\n return;\n }\n const top = this.viewport.getBoundingClientRect().top + this.viewport.clientTop;\n const delta = this._ref.getBoundingClientRect().top - top - this._refOffset;\n if (Math.abs(delta) >= 1) this.viewport.scrollTop += delta;\n }\n\n /** Sizes the blank space that lets the latest anchored turn reach the top. */\n private _updateSpacer() {\n let space = 0;\n if (this._anchor?.parentElement === this.viewport) {\n const room = this._top(this._anchor) - this.options.peek + this.viewport.clientHeight;\n space = Math.max(0, Math.ceil(room - this._contentEnd()));\n } else this._anchor = null;\n if (space === this._spacer) return;\n this._spacer = space;\n if (space) this.viewport.style.setProperty('--md-comp-message-scroller-spacer', `${space}px`);\n else this.viewport.style.removeProperty('--md-comp-message-scroller-spacer');\n }\n\n /** The button is inert while there is nothing below. */\n private _sync() {\n if (!this.button) return;\n const idle = this._atEnd();\n // The button's own root: inside a shadow root, document.activeElement is the host.\n const root = this.button.getRootNode() as Document | ShadowRoot;\n if (idle && root.activeElement === this.button) this.viewport.focus({ preventScroll: true });\n this.button.inert = idle;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,270 @@
1
+ import {
2
+ Utils
3
+ } from "./chunk-6OMRGML6.mjs";
4
+ import {
5
+ Component
6
+ } from "./chunk-LTWPGFBR.mjs";
7
+
8
+ // src/ts/components/questionnaire.ts
9
+ var _defaults = {
10
+ progressLabel: "Question {current} of {total}",
11
+ requiredMessage: "Choose an answer to continue."
12
+ };
13
+ var NAVIGATION = ["previous", "skip", "next"];
14
+ var Questionnaire = class _Questionnaire extends Component {
15
+ constructor(el, options) {
16
+ super(el, options, _Questionnaire);
17
+ this._active = null;
18
+ this._generated = [];
19
+ this._generatedIds = [];
20
+ /** Set while one submission's invalid events arrive, so only the first opens. */
21
+ this._reporting = false;
22
+ /** Shows the question at `index` without checking the current one. */
23
+ this.goTo = (index) => {
24
+ const item = this.items[index];
25
+ if (!item) return;
26
+ this._active = item;
27
+ this._render();
28
+ const controls = this._controls(item).filter((control) => !control.disabled);
29
+ (controls.find((control) => control.checked) ?? controls[0])?.focus();
30
+ };
31
+ /** Checks the shown question and moves to the next one if it is answered. */
32
+ this.next = () => {
33
+ if (this._validate(this._active)) this.goTo(this.index + 1);
34
+ };
35
+ /** Moves to the previous question. */
36
+ this.previous = () => this.goTo(this.index - 1);
37
+ /** Clears the answer to the shown question and moves to the next one. */
38
+ this.skip = () => {
39
+ if (!this._active) return;
40
+ for (const control of this._controls(this._active)) {
41
+ const isChoice = control instanceof HTMLInputElement && (control.type === "radio" || control.type === "checkbox");
42
+ if (isChoice ? !control.checked : !control.value) continue;
43
+ if (isChoice) control.checked = false;
44
+ else control.value = "";
45
+ control.dispatchEvent(new Event("change", { bubbles: true }));
46
+ }
47
+ this.goTo(this.index + 1);
48
+ };
49
+ this._handleClick = (e) => {
50
+ const button = e.target.closest?.("button");
51
+ if (!button || !this._buttons().includes(button)) return;
52
+ if (NAVIGATION.includes(button.value)) {
53
+ e.preventDefault();
54
+ this[button.value]();
55
+ return;
56
+ }
57
+ this._syncRequired();
58
+ };
59
+ // Enter in a field moves on instead of submitting the form early.
60
+ this._handleKeydown = (e) => {
61
+ if (e.key !== "Enter" || e.defaultPrevented || e.isComposing) return;
62
+ if (!(e.target instanceof HTMLInputElement) || this.index >= this.items.length - 1) return;
63
+ e.preventDefault();
64
+ this.next();
65
+ };
66
+ // A submission found an unanswered question, possibly a hidden one. Open the
67
+ // first instead of letting the browser report a control it cannot show.
68
+ // One submission fires its invalid events in a single task.
69
+ this._handleInvalid = (e) => {
70
+ const item = this.items.find((item2) => item2.contains(e.target));
71
+ if (!item) return;
72
+ e.preventDefault();
73
+ if (this._reporting) return;
74
+ this._reporting = true;
75
+ setTimeout(() => this._reporting = false);
76
+ this._active = item;
77
+ this._render();
78
+ this._validate(item);
79
+ };
80
+ // An answer clears the question's error.
81
+ this._handleInput = (e) => {
82
+ this._syncRequired();
83
+ const item = e.target.closest?.("fieldset");
84
+ if (item instanceof HTMLFieldSetElement && item.classList.contains("invalid")) this._setInvalid(item, false);
85
+ };
86
+ this.el["Expressive_Questionnaire"] = this;
87
+ const progressLabel = el.dataset.progressLabel;
88
+ this.options = {
89
+ ..._Questionnaire.defaults,
90
+ ...progressLabel ? { progressLabel } : {},
91
+ ...options
92
+ };
93
+ this._progress = document.createElement("div");
94
+ this._progress.className = "questionnaire-progress";
95
+ this._progressText = document.createElement("span");
96
+ this._progressText.id = `questionnaire-progress-${Utils.guid()}`;
97
+ this._progressBar = document.createElement("progress");
98
+ this._progressBar.className = "progress";
99
+ this._progressBar.setAttribute("aria-labelledby", this._progressText.id);
100
+ this._progress.append(this._progressText, this._progressBar);
101
+ this.el.prepend(this._progress);
102
+ for (const item of this._fieldsets()) {
103
+ const ids = Array.from(item.querySelectorAll(":scope > p:not(.questionnaire-error)")).map((p) => this._ensureId(p, "questionnaire-description"));
104
+ if (ids.length && !item.hasAttribute("aria-describedby")) {
105
+ item.setAttribute("aria-describedby", ids.join(" "));
106
+ this._generatedIds.push(item);
107
+ }
108
+ }
109
+ this._active = this.items[0] ?? null;
110
+ this._syncRequired();
111
+ this._render();
112
+ this._observer = new MutationObserver(() => {
113
+ this._syncRequired();
114
+ this._render();
115
+ });
116
+ this._observer.observe(this.el, { attributes: true, subtree: true, attributeFilter: ["disabled"] });
117
+ this.el.addEventListener("click", this._handleClick);
118
+ this.el.addEventListener("keydown", this._handleKeydown);
119
+ this.el.addEventListener("input", this._handleInput);
120
+ this.el.addEventListener("invalid", this._handleInvalid, true);
121
+ }
122
+ static get defaults() {
123
+ return _defaults;
124
+ }
125
+ /**
126
+ * Initializes instances of Questionnaire.
127
+ * @param els HTML elements.
128
+ * @param options Component options.
129
+ */
130
+ static init(els, options = {}) {
131
+ return super.init(els, options, _Questionnaire);
132
+ }
133
+ static getInstance(el) {
134
+ return el["Expressive_Questionnaire"];
135
+ }
136
+ /** The questions that apply: every fieldset child that is not disabled. */
137
+ get items() {
138
+ return this._fieldsets().filter((item) => !item.disabled);
139
+ }
140
+ /** Position of the shown question in `items`. */
141
+ get index() {
142
+ return this.items.indexOf(this._active);
143
+ }
144
+ destroy() {
145
+ this._observer.disconnect();
146
+ this.el.removeEventListener("click", this._handleClick);
147
+ this.el.removeEventListener("keydown", this._handleKeydown);
148
+ this.el.removeEventListener("input", this._handleInput);
149
+ this.el.removeEventListener("invalid", this._handleInvalid, true);
150
+ for (const item of this._fieldsets()) {
151
+ if (item.hasAttribute("data-required")) for (const control of this._controls(item)) control.setCustomValidity("");
152
+ item.hidden = false;
153
+ this._setInvalid(item, false);
154
+ }
155
+ for (const button of this._buttons()) button.hidden = false;
156
+ this._progress.remove();
157
+ for (const el of this._generated) el.remove();
158
+ for (const el of this._generatedIds) {
159
+ el.removeAttribute(el instanceof HTMLFieldSetElement ? "aria-describedby" : "id");
160
+ }
161
+ this.el["Expressive_Questionnaire"] = void 0;
162
+ }
163
+ _fieldsets() {
164
+ return Array.from(this.el.querySelectorAll(":scope > fieldset"));
165
+ }
166
+ /** The footer's navigation and submit buttons. */
167
+ _buttons() {
168
+ return Array.from(this.el.querySelectorAll(":scope > footer > button")).filter(
169
+ (button) => NAVIGATION.includes(button.value) || button.type === "submit"
170
+ );
171
+ }
172
+ _controls(item) {
173
+ return Array.from(item.elements).filter(
174
+ (el) => el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement || el instanceof HTMLInputElement && el.type !== "button" && el.type !== "submit"
175
+ );
176
+ }
177
+ _required(item) {
178
+ return item.hasAttribute("data-required") || this._controls(item).some((control) => control.required);
179
+ }
180
+ _ensureId(el, prefix) {
181
+ if (!el.id) {
182
+ el.id = `${prefix}-${Utils.guid()}`;
183
+ this._generatedIds.push(el);
184
+ }
185
+ return el.id;
186
+ }
187
+ _render() {
188
+ const items = this.items;
189
+ if (!items.includes(this._active)) {
190
+ const all = this._fieldsets();
191
+ const after = all.slice(all.indexOf(this._active) + 1).find((item) => !item.disabled);
192
+ this._active = after ?? items[items.length - 1] ?? null;
193
+ }
194
+ const index = items.indexOf(this._active);
195
+ const last = index === items.length - 1;
196
+ for (const item of this._fieldsets()) item.hidden = item !== this._active;
197
+ for (const button of this._buttons()) {
198
+ if (button.value === "previous") button.hidden = index <= 0;
199
+ else if (button.value === "skip") button.hidden = last || !this._active || this._required(this._active);
200
+ else if (button.value === "next") button.hidden = last;
201
+ else button.hidden = !last;
202
+ }
203
+ this._progressText.textContent = this.options.progressLabel.replace("{current}", String(index + 1)).replace("{total}", String(items.length));
204
+ this._progressBar.max = Math.max(items.length, 1);
205
+ this._progressBar.value = index + 1;
206
+ }
207
+ /**
208
+ * Puts "choose at least one" on the first enabled checkbox of each
209
+ * data-required group with none checked, so the browser's validation, and
210
+ * every way of submitting, sees it. A disabled checkbox is not sent, so it
211
+ * does not count.
212
+ */
213
+ _syncRequired() {
214
+ for (const item of this._fieldsets()) {
215
+ if (!item.hasAttribute("data-required")) continue;
216
+ const boxes = this._controls(item).filter(
217
+ (control) => control instanceof HTMLInputElement && control.type === "checkbox"
218
+ );
219
+ const enabled = boxes.filter((box) => !box.disabled);
220
+ for (const box of boxes) box.setCustomValidity("");
221
+ if (enabled.length && !enabled.some((box) => box.checked)) {
222
+ const error = item.querySelector(":scope > .questionnaire-error:not([data-generated])");
223
+ enabled[0].setCustomValidity(error?.textContent.trim() || this.options.requiredMessage);
224
+ }
225
+ }
226
+ }
227
+ /** The first control in the question that still needs an answer. */
228
+ _invalidControl(item) {
229
+ return this._controls(item).find((control) => control.willValidate && !control.validity.valid);
230
+ }
231
+ /** Shows the question's error and returns false when it needs an answer. */
232
+ _validate(item) {
233
+ this._syncRequired();
234
+ const invalid = item && this._invalidControl(item);
235
+ if (!invalid) return true;
236
+ let error = item.querySelector(":scope > .questionnaire-error");
237
+ if (!error) {
238
+ error = document.createElement("p");
239
+ error.className = "questionnaire-error";
240
+ error.dataset.generated = "";
241
+ error.textContent = invalid.validationMessage || this.options.requiredMessage;
242
+ item.append(error);
243
+ this._generated.push(error);
244
+ }
245
+ this._setInvalid(item, true);
246
+ invalid.focus();
247
+ return false;
248
+ }
249
+ _setInvalid(item, invalid) {
250
+ item.classList.toggle("invalid", invalid);
251
+ const error = item.querySelector(":scope > .questionnaire-error");
252
+ const described = (item.getAttribute("aria-describedby") ?? "").split(" ").filter((id) => id && id !== error?.id);
253
+ if (invalid && error) described.push(this._ensureId(error, "questionnaire-error"));
254
+ if (described.length) item.setAttribute("aria-describedby", described.join(" "));
255
+ else item.removeAttribute("aria-describedby");
256
+ for (const control of this._controls(item)) {
257
+ if (invalid) control.setAttribute("aria-invalid", "true");
258
+ else control.removeAttribute("aria-invalid");
259
+ }
260
+ if (!invalid && this._generated.includes(error)) {
261
+ error.remove();
262
+ this._generated.splice(this._generated.indexOf(error), 1);
263
+ }
264
+ }
265
+ };
266
+
267
+ export {
268
+ Questionnaire
269
+ };
270
+ //# sourceMappingURL=chunk-DTKR54I6.mjs.map