stimeo-ui 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -5
- data/dist/cable/index.js +60 -14
- data/dist/controllers/announcer_controller.js +88 -61
- data/dist/controllers/auto_submit_controller.js +40 -5
- data/dist/controllers/breadcrumb_controller.js +29 -3
- data/dist/controllers/calendar_controller.js +51 -10
- data/dist/controllers/carousel_controller.js +8 -6
- data/dist/controllers/checkbox_controller.js +27 -2
- data/dist/controllers/conditional_fields_controller.js +36 -7
- data/dist/controllers/currency_input_controller.js +77 -25
- data/dist/controllers/date_range_picker_controller.js +43 -10
- data/dist/controllers/direct_upload_controller.js +21 -10
- data/dist/controllers/dirty_form_controller.js +32 -12
- data/dist/controllers/flash_controller.js +145 -43
- data/dist/controllers/form_field_controller.js +7 -1
- data/dist/controllers/frame_loading_controller.js +29 -6
- data/dist/controllers/highlight_controller.js +63 -23
- data/dist/controllers/input_mask_controller.js +33 -12
- data/dist/controllers/intersection_controller.js +10 -5
- data/dist/controllers/lazy_frame_controller.js +10 -5
- data/dist/controllers/local_time_controller.js +43 -11
- data/dist/controllers/masonry_controller.js +32 -14
- data/dist/controllers/menu_controller.js +10 -2
- data/dist/controllers/menubar_controller.js +10 -2
- data/dist/controllers/navigation_menu_controller.js +8 -2
- data/dist/controllers/optimistic_controller.js +28 -4
- data/dist/controllers/otp_controller.js +23 -10
- data/dist/controllers/overflow_indicator_controller.js +31 -8
- data/dist/controllers/overflow_menu_controller.js +39 -4
- data/dist/controllers/pointer_drag_controller.js +10 -3
- data/dist/controllers/portal_controller.js +21 -10
- data/dist/controllers/radio_group_controller.js +11 -3
- data/dist/controllers/read_more_controller.js +31 -5
- data/dist/controllers/reading_progress_controller.js +65 -19
- data/dist/controllers/relative_time_controller.js +40 -9
- data/dist/controllers/reset_before_cache_controller.js +23 -10
- data/dist/controllers/resizable_controller.js +7 -2
- data/dist/controllers/roving_controller.js +7 -3
- data/dist/controllers/scroll_area_controller.js +70 -26
- data/dist/controllers/scroll_restore_controller.js +37 -16
- data/dist/controllers/scroll_visibility_controller.js +71 -42
- data/dist/controllers/scrollspy_controller.js +76 -32
- data/dist/controllers/smart_sticky_header_controller.js +72 -37
- data/dist/controllers/stick_to_bottom_controller.js +29 -3
- data/dist/controllers/sticky_observer_controller.js +10 -5
- data/dist/controllers/submit_once_controller.js +29 -6
- data/dist/controllers/textarea_autosize_controller.js +29 -3
- data/dist/controllers/theme_controller.js +23 -13
- data/dist/controllers/toast_controller.js +136 -73
- data/dist/controllers/toggle_group_controller.js +11 -3
- data/dist/controllers/toolbar_controller.js +7 -3
- data/dist/index.js +629 -597
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -18,6 +18,35 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
18
18
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// src/utils/intl_format.ts
|
|
22
|
+
var formatters = /* @__PURE__ */ new WeakMap();
|
|
23
|
+
function intlFormatter(factory, locale, options, fallbackLocale) {
|
|
24
|
+
const cache = cacheFor(factory);
|
|
25
|
+
const key = cacheKey(locale, options, fallbackLocale);
|
|
26
|
+
if (cache.has(key)) return cache.get(key);
|
|
27
|
+
const formatter = construct(factory, locale, options, fallbackLocale);
|
|
28
|
+
cache.set(key, formatter);
|
|
29
|
+
return formatter;
|
|
30
|
+
}
|
|
31
|
+
function cacheFor(factory) {
|
|
32
|
+
const existing = formatters.get(factory);
|
|
33
|
+
if (existing) return existing;
|
|
34
|
+
const created = /* @__PURE__ */ new Map();
|
|
35
|
+
formatters.set(factory, created);
|
|
36
|
+
return created;
|
|
37
|
+
}
|
|
38
|
+
function cacheKey(locale, options, fallbackLocale) {
|
|
39
|
+
return JSON.stringify([locale, options, fallbackLocale]);
|
|
40
|
+
}
|
|
41
|
+
function construct(factory, locale, options, fallbackLocale) {
|
|
42
|
+
try {
|
|
43
|
+
return new factory(locale, options);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (!(error instanceof RangeError)) throw error;
|
|
46
|
+
return new factory(fallbackLocale, options);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
21
50
|
// src/utils/dates.ts
|
|
22
51
|
function toISODateString(date) {
|
|
23
52
|
const year = date.getFullYear();
|
|
@@ -51,14 +80,14 @@ function parseISOMonthString(monthStr) {
|
|
|
51
80
|
function toISOMonthString(date) {
|
|
52
81
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
53
82
|
}
|
|
83
|
+
var MONTH_LABEL_OPTIONS = { month: "long", year: "numeric" };
|
|
54
84
|
function monthLabelFormatter(locale) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
85
|
+
return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/utils/locale.ts
|
|
89
|
+
function resolveLocale(element, declared = "") {
|
|
90
|
+
return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
// src/utils/safe_timeout.ts
|
|
@@ -123,8 +152,9 @@ var CalendarController = class extends Controller {
|
|
|
123
152
|
selected: { type: String, default: "" },
|
|
124
153
|
min: { type: String, default: "" },
|
|
125
154
|
max: { type: String, default: "" },
|
|
126
|
-
weekStart: { type: Number, default: 0 }
|
|
155
|
+
weekStart: { type: Number, default: 0 },
|
|
127
156
|
// 0 = Sunday, 1 = Monday, etc.
|
|
157
|
+
locale: { type: String, default: "" }
|
|
128
158
|
};
|
|
129
159
|
static actions = ["next", "onKeydown", "prev", "selectByClick"];
|
|
130
160
|
static events = ["monthchange", "select"];
|
|
@@ -166,6 +196,17 @@ var CalendarController = class extends Controller {
|
|
|
166
196
|
this.#syncFocusedDateWithMonth();
|
|
167
197
|
this.render();
|
|
168
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Repaints the label when application code changes `locale` at runtime.
|
|
201
|
+
*
|
|
202
|
+
* Stimulus runs this for the initial Value too, before `connect()`. The paint
|
|
203
|
+
* it triggers is idempotent — the month comes from `month` (or, while that is
|
|
204
|
+
* still empty, from `focusedDate`), and `monthchange` only reports a move away
|
|
205
|
+
* from a month already announced, so the first paint never reports one.
|
|
206
|
+
*/
|
|
207
|
+
localeValueChanged() {
|
|
208
|
+
this.render();
|
|
209
|
+
}
|
|
169
210
|
/**
|
|
170
211
|
* Stimulus lifecycle callback triggered automatically when the selectedValue changes.
|
|
171
212
|
* Re-renders grid cells to update `aria-selected` indicators.
|
|
@@ -296,8 +337,8 @@ var CalendarController = class extends Controller {
|
|
|
296
337
|
const year = monthStart.getFullYear();
|
|
297
338
|
const month = monthStart.getMonth() + 1;
|
|
298
339
|
if (this.hasLabelTarget) {
|
|
299
|
-
const
|
|
300
|
-
this.labelTarget.textContent = monthLabelFormatter(
|
|
340
|
+
const locale = resolveLocale(this.element, this.localeValue);
|
|
341
|
+
this.labelTarget.textContent = monthLabelFormatter(locale).format(monthStart);
|
|
301
342
|
}
|
|
302
343
|
const days = this.#calculateGridDays(year, month);
|
|
303
344
|
const dayElements = this.dayTargets;
|
|
@@ -90,6 +90,12 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
90
90
|
}
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
// src/utils/event_owner.ts
|
|
94
|
+
function ownerIndex(candidates, node) {
|
|
95
|
+
if (!(node instanceof Node)) return -1;
|
|
96
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
// src/utils/microtask_coalescer.ts
|
|
94
100
|
var MicrotaskCoalescer = class {
|
|
95
101
|
#run;
|
|
@@ -510,9 +516,7 @@ var CarouselController = class extends Controller {
|
|
|
510
516
|
}
|
|
511
517
|
/** Position of the picker that is or contains `node`, or `-1` when none does. */
|
|
512
518
|
#pickerIndexFor(node) {
|
|
513
|
-
return this.pickerTargets
|
|
514
|
-
(picker) => node instanceof Node && (picker === node || picker.contains(node))
|
|
515
|
-
);
|
|
519
|
+
return ownerIndex(this.pickerTargets, node);
|
|
516
520
|
}
|
|
517
521
|
/** Whether a focus transition lands on another control of this same carousel. */
|
|
518
522
|
#focusStaysInside(event) {
|
|
@@ -731,9 +735,7 @@ function hasModifier(event) {
|
|
|
731
735
|
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
732
736
|
}
|
|
733
737
|
function hits(elements, node) {
|
|
734
|
-
return elements
|
|
735
|
-
(element) => node instanceof Node && (element === node || element.contains(node))
|
|
736
|
-
);
|
|
738
|
+
return ownerIndex(elements, node) !== -1;
|
|
737
739
|
}
|
|
738
740
|
function setAttributeIfChanged(element, name, value) {
|
|
739
741
|
if (element.getAttribute(name) !== value) element.setAttribute(name, value);
|
|
@@ -2,6 +2,30 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/checkbox_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/listener_set.ts
|
|
6
|
+
var ListenerSet = class {
|
|
7
|
+
/** The generation every `add` joins until the next `dispose()`. */
|
|
8
|
+
#abort = new AbortController();
|
|
9
|
+
/**
|
|
10
|
+
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
11
|
+
*
|
|
12
|
+
* The set supplies the signal, so `options` carries everything else the DOM
|
|
13
|
+
* accepts — `capture` included, which has to match at release time and no
|
|
14
|
+
* longer has a second place to drift from.
|
|
15
|
+
*/
|
|
16
|
+
add(target, type, handler, options) {
|
|
17
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Releases every listener of the open generation, synchronously, and opens the
|
|
21
|
+
* next one. Idempotent, and safe before anything has been added.
|
|
22
|
+
*/
|
|
23
|
+
dispose() {
|
|
24
|
+
this.#abort.abort();
|
|
25
|
+
this.#abort = new AbortController();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
5
29
|
// src/utils/microtask_coalescer.ts
|
|
6
30
|
var MicrotaskCoalescer = class {
|
|
7
31
|
#run;
|
|
@@ -42,6 +66,7 @@ var CheckboxController = class extends Controller {
|
|
|
42
66
|
static events = ["change", "reconcile"];
|
|
43
67
|
/** Collapses every lifecycle signal from one DOM update into one derived pass. */
|
|
44
68
|
#reconcile = new MicrotaskCoalescer(() => this.#reconcileFromChildren());
|
|
69
|
+
#listeners = new ListenerSet();
|
|
45
70
|
/** Aggregate this root last settled on, so a derived repair is reported once. */
|
|
46
71
|
#committedState = null;
|
|
47
72
|
/** Watches authored checked-attribute changes on retained target elements. */
|
|
@@ -59,14 +84,14 @@ var CheckboxController = class extends Controller {
|
|
|
59
84
|
attributeFilter: ["checked"],
|
|
60
85
|
subtree: true
|
|
61
86
|
});
|
|
62
|
-
this.element
|
|
87
|
+
this.#listeners.add(this.element, "turbo:morph-element", this.#onMorph);
|
|
63
88
|
document.addEventListener("reset", this.#onReset, true);
|
|
64
89
|
}
|
|
65
90
|
/** Releases the observer, global reset listener, and every pending reconciliation. */
|
|
66
91
|
disconnect() {
|
|
67
92
|
this.#reconcile.cancel();
|
|
68
93
|
this.#checkedObserver.disconnect();
|
|
69
|
-
this.
|
|
94
|
+
this.#listeners.dispose();
|
|
70
95
|
document.removeEventListener("reset", this.#onReset, true);
|
|
71
96
|
}
|
|
72
97
|
/** Reconciles the aggregate for a parent added or replaced at runtime. */
|
|
@@ -2,6 +2,12 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/conditional_fields_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/event_owner.ts
|
|
6
|
+
function ownerIndex(candidates, node) {
|
|
7
|
+
if (!(node instanceof Node)) return -1;
|
|
8
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
// src/utils/focus_candidate.ts
|
|
6
12
|
function inheritsFieldsetDisabled(control) {
|
|
7
13
|
let fieldset = control.closest("fieldset[disabled]");
|
|
@@ -20,6 +26,30 @@ function canTakeFocus(element) {
|
|
|
20
26
|
return !inheritsFieldsetDisabled(element);
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
// src/utils/listener_set.ts
|
|
30
|
+
var ListenerSet = class {
|
|
31
|
+
/** The generation every `add` joins until the next `dispose()`. */
|
|
32
|
+
#abort = new AbortController();
|
|
33
|
+
/**
|
|
34
|
+
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
35
|
+
*
|
|
36
|
+
* The set supplies the signal, so `options` carries everything else the DOM
|
|
37
|
+
* accepts — `capture` included, which has to match at release time and no
|
|
38
|
+
* longer has a second place to drift from.
|
|
39
|
+
*/
|
|
40
|
+
add(target, type, handler, options) {
|
|
41
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Releases every listener of the open generation, synchronously, and opens the
|
|
45
|
+
* next one. Idempotent, and safe before anything has been added.
|
|
46
|
+
*/
|
|
47
|
+
dispose() {
|
|
48
|
+
this.#abort.abort();
|
|
49
|
+
this.#abort = new AbortController();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
23
53
|
// src/utils/microtask_coalescer.ts
|
|
24
54
|
var MicrotaskCoalescer = class {
|
|
25
55
|
#run;
|
|
@@ -144,6 +174,7 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
144
174
|
#observing = false;
|
|
145
175
|
/** Coalesces one target/morph/mutation batch into one full DOM reconciliation. */
|
|
146
176
|
#reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
|
|
177
|
+
#listeners = new ListenerSet();
|
|
147
178
|
/** Provides the last-resort focus landmark without claiming an authored tabindex. */
|
|
148
179
|
#rootTabindex = new TabindexLoan();
|
|
149
180
|
/** Watches retained targets and controls whose declarative or reflected state changed. */
|
|
@@ -182,9 +213,9 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
182
213
|
subtree: true
|
|
183
214
|
});
|
|
184
215
|
this.#observing = true;
|
|
185
|
-
this.element
|
|
186
|
-
this.element
|
|
187
|
-
this.element
|
|
216
|
+
this.#listeners.add(this.element, "change", this.#onTriggerInput);
|
|
217
|
+
this.#listeners.add(this.element, "input", this.#onTriggerInput);
|
|
218
|
+
this.#listeners.add(this.element, "turbo:morph-element", this.#onMorph);
|
|
188
219
|
document.addEventListener("reset", this.#onReset, true);
|
|
189
220
|
}
|
|
190
221
|
/** Releases all external resources and returns the temporary focus landmark loan. */
|
|
@@ -192,9 +223,7 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
192
223
|
this.#reconcile.cancel();
|
|
193
224
|
this.#observing = false;
|
|
194
225
|
this.#observer.disconnect();
|
|
195
|
-
this.
|
|
196
|
-
this.element.removeEventListener("input", this.#onTriggerInput);
|
|
197
|
-
this.element.removeEventListener("turbo:morph-element", this.#onMorph);
|
|
226
|
+
this.#listeners.dispose();
|
|
198
227
|
document.removeEventListener("reset", this.#onReset, true);
|
|
199
228
|
this.#rootTabindex.returnAll();
|
|
200
229
|
}
|
|
@@ -393,7 +422,7 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
393
422
|
}
|
|
394
423
|
/** Whether an authored mutation target belongs to a trigger, region, or managed control. */
|
|
395
424
|
#elementAffectsState(target, triggers, regions) {
|
|
396
|
-
return triggers
|
|
425
|
+
return ownerIndex(triggers, target) !== -1 || ownerIndex(regions, target) !== -1;
|
|
397
426
|
}
|
|
398
427
|
/** Whether a form owns at least one current trigger, including `form=` associations. */
|
|
399
428
|
#hasTriggerOwnedBy(form) {
|
|
@@ -58,11 +58,47 @@ function halfWidthChar(char) {
|
|
|
58
58
|
return char === "\u3000" ? " " : char;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// src/utils/intl_format.ts
|
|
62
|
+
var formatters = /* @__PURE__ */ new WeakMap();
|
|
63
|
+
function intlFormatter(factory, locale, options, fallbackLocale) {
|
|
64
|
+
const cache = cacheFor(factory);
|
|
65
|
+
const key = cacheKey(locale, options, fallbackLocale);
|
|
66
|
+
if (cache.has(key)) return cache.get(key);
|
|
67
|
+
const formatter = construct(factory, locale, options, fallbackLocale);
|
|
68
|
+
cache.set(key, formatter);
|
|
69
|
+
return formatter;
|
|
70
|
+
}
|
|
71
|
+
function cacheFor(factory) {
|
|
72
|
+
const existing = formatters.get(factory);
|
|
73
|
+
if (existing) return existing;
|
|
74
|
+
const created = /* @__PURE__ */ new Map();
|
|
75
|
+
formatters.set(factory, created);
|
|
76
|
+
return created;
|
|
77
|
+
}
|
|
78
|
+
function cacheKey(locale, options, fallbackLocale) {
|
|
79
|
+
return JSON.stringify([locale, options, fallbackLocale]);
|
|
80
|
+
}
|
|
81
|
+
function construct(factory, locale, options, fallbackLocale) {
|
|
82
|
+
try {
|
|
83
|
+
return new factory(locale, options);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!(error instanceof RangeError)) throw error;
|
|
86
|
+
if (fallbackLocale === void 0) return null;
|
|
87
|
+
return new factory(fallbackLocale, options);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/utils/locale.ts
|
|
92
|
+
function resolveLocale(element, declared = "") {
|
|
93
|
+
return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
61
96
|
// src/controllers/currency_input_controller.ts
|
|
97
|
+
var FALLBACK_LOCALE = "en-US";
|
|
62
98
|
var CurrencyInputController = class extends Controller {
|
|
63
99
|
static targets = ["display", "field", "srValue"];
|
|
64
100
|
static values = {
|
|
65
|
-
locale: { type: String, default: "
|
|
101
|
+
locale: { type: String, default: "" },
|
|
66
102
|
currency: { type: String, default: "" },
|
|
67
103
|
precision: { type: Number, default: 2 }
|
|
68
104
|
};
|
|
@@ -72,7 +108,7 @@ var CurrencyInputController = class extends Controller {
|
|
|
72
108
|
#lastValue = null;
|
|
73
109
|
#started = false;
|
|
74
110
|
/** Validated mirrors of the Values; the hot path never reads a raw Value. */
|
|
75
|
-
#locale
|
|
111
|
+
#locale;
|
|
76
112
|
#currency = "";
|
|
77
113
|
#precision = 2;
|
|
78
114
|
/** Formatters rebuilt only when a Value changes, never per keystroke. */
|
|
@@ -309,37 +345,53 @@ var CurrencyInputController = class extends Controller {
|
|
|
309
345
|
* formatters from the validated set.
|
|
310
346
|
*/
|
|
311
347
|
#revalidate() {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
this.#locale = this.localeValue;
|
|
316
|
-
} catch {
|
|
317
|
-
}
|
|
348
|
+
const resolved = resolveLocale(this.element, this.localeValue);
|
|
349
|
+
const usable = intlFormatter(Intl.NumberFormat, resolved, {});
|
|
350
|
+
this.#locale = usable === null ? FALLBACK_LOCALE : resolved;
|
|
318
351
|
const precision = this.precisionValue;
|
|
319
352
|
this.#precision = Number.isInteger(precision) && precision >= 0 && precision <= 100 ? precision : 2;
|
|
320
353
|
this.#currency = "";
|
|
321
354
|
if (this.currencyValue !== "") {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
355
|
+
const declaredCurrency = intlFormatter(Intl.NumberFormat, this.#locale, {
|
|
356
|
+
style: "currency",
|
|
357
|
+
currency: this.currencyValue
|
|
358
|
+
});
|
|
359
|
+
if (declaredCurrency !== null) this.#currency = this.currencyValue;
|
|
327
360
|
}
|
|
328
|
-
this.#grouping =
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
361
|
+
this.#grouping = intlFormatter(
|
|
362
|
+
Intl.NumberFormat,
|
|
363
|
+
this.#locale,
|
|
364
|
+
{ useGrouping: true, maximumFractionDigits: 0 },
|
|
365
|
+
FALLBACK_LOCALE
|
|
366
|
+
);
|
|
367
|
+
this.#fixed = intlFormatter(
|
|
368
|
+
Intl.NumberFormat,
|
|
369
|
+
this.#locale,
|
|
370
|
+
{
|
|
371
|
+
useGrouping: true,
|
|
372
|
+
minimumFractionDigits: this.#precision,
|
|
373
|
+
maximumFractionDigits: this.#precision
|
|
374
|
+
},
|
|
375
|
+
FALLBACK_LOCALE
|
|
376
|
+
);
|
|
377
|
+
this.#accessible = this.#currency ? intlFormatter(
|
|
378
|
+
Intl.NumberFormat,
|
|
379
|
+
this.#locale,
|
|
380
|
+
{ style: "currency", currency: this.#currency },
|
|
381
|
+
FALLBACK_LOCALE
|
|
382
|
+
) : this.#fixed;
|
|
383
|
+
const parts = intlFormatter(Intl.NumberFormat, this.#locale, {}, FALLBACK_LOCALE).formatToParts(
|
|
384
|
+
11111.1
|
|
385
|
+
);
|
|
339
386
|
this.#group = parts.find((p) => p.type === "group")?.value ?? ",";
|
|
340
387
|
this.#decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
|
|
341
388
|
this.#digits.clear();
|
|
342
|
-
const digitFormatter =
|
|
389
|
+
const digitFormatter = intlFormatter(
|
|
390
|
+
Intl.NumberFormat,
|
|
391
|
+
this.#locale,
|
|
392
|
+
{ useGrouping: false },
|
|
393
|
+
FALLBACK_LOCALE
|
|
394
|
+
);
|
|
343
395
|
for (let i = 0; i <= 9; i++) {
|
|
344
396
|
const digit = digitFormatter.format(i);
|
|
345
397
|
if (digit !== String(i)) this.#digits.set(digit, String(i));
|
|
@@ -47,6 +47,35 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
// src/utils/intl_format.ts
|
|
51
|
+
var formatters = /* @__PURE__ */ new WeakMap();
|
|
52
|
+
function intlFormatter(factory, locale, options, fallbackLocale) {
|
|
53
|
+
const cache = cacheFor(factory);
|
|
54
|
+
const key = cacheKey(locale, options, fallbackLocale);
|
|
55
|
+
if (cache.has(key)) return cache.get(key);
|
|
56
|
+
const formatter = construct(factory, locale, options, fallbackLocale);
|
|
57
|
+
cache.set(key, formatter);
|
|
58
|
+
return formatter;
|
|
59
|
+
}
|
|
60
|
+
function cacheFor(factory) {
|
|
61
|
+
const existing = formatters.get(factory);
|
|
62
|
+
if (existing) return existing;
|
|
63
|
+
const created = /* @__PURE__ */ new Map();
|
|
64
|
+
formatters.set(factory, created);
|
|
65
|
+
return created;
|
|
66
|
+
}
|
|
67
|
+
function cacheKey(locale, options, fallbackLocale) {
|
|
68
|
+
return JSON.stringify([locale, options, fallbackLocale]);
|
|
69
|
+
}
|
|
70
|
+
function construct(factory, locale, options, fallbackLocale) {
|
|
71
|
+
try {
|
|
72
|
+
return new factory(locale, options);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (!(error instanceof RangeError)) throw error;
|
|
75
|
+
return new factory(fallbackLocale, options);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
50
79
|
// src/utils/dates.ts
|
|
51
80
|
function toISODateString(date) {
|
|
52
81
|
const year = date.getFullYear();
|
|
@@ -80,14 +109,14 @@ function parseISOMonthString(monthStr) {
|
|
|
80
109
|
function toISOMonthString(date) {
|
|
81
110
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
82
111
|
}
|
|
112
|
+
var MONTH_LABEL_OPTIONS = { month: "long", year: "numeric" };
|
|
83
113
|
function monthLabelFormatter(locale) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
114
|
+
return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/utils/locale.ts
|
|
118
|
+
function resolveLocale(element, declared = "") {
|
|
119
|
+
return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
91
120
|
}
|
|
92
121
|
|
|
93
122
|
// src/utils/microtask_coalescer.ts
|
|
@@ -200,7 +229,8 @@ var DateRangePickerController = class extends Controller {
|
|
|
200
229
|
// A JSON list read through `parseStringList` rather than Stimulus's `Array`
|
|
201
230
|
// type: that reader throws out of the value observer before any callback
|
|
202
231
|
// runs, so one malformed attribute would stop the picker from connecting.
|
|
203
|
-
disabledDates: { type: String, default: "" }
|
|
232
|
+
disabledDates: { type: String, default: "" },
|
|
233
|
+
locale: { type: String, default: "" }
|
|
204
234
|
};
|
|
205
235
|
static actions = ["applyPreset", "next", "onKeydown", "prev", "previewTo", "selectDate"];
|
|
206
236
|
static events = ["change", "monthchange"];
|
|
@@ -253,6 +283,10 @@ var DateRangePickerController = class extends Controller {
|
|
|
253
283
|
this.#beforeCache.deactivate();
|
|
254
284
|
this.#focusTimer.clearAll();
|
|
255
285
|
}
|
|
286
|
+
/** Repaints the label when application code changes `locale` at runtime. */
|
|
287
|
+
localeValueChanged() {
|
|
288
|
+
this.#repaint.schedule();
|
|
289
|
+
}
|
|
256
290
|
/** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
|
|
257
291
|
minValueChanged() {
|
|
258
292
|
this.#repaint.schedule();
|
|
@@ -441,8 +475,7 @@ var DateRangePickerController = class extends Controller {
|
|
|
441
475
|
const { year, month } = info;
|
|
442
476
|
if (this.#previewDate && !this.#isSelectable(this.#previewDate)) this.#previewDate = "";
|
|
443
477
|
if (this.hasMonthLabelTarget) {
|
|
444
|
-
const
|
|
445
|
-
const formatter = monthLabelFormatter(lang);
|
|
478
|
+
const formatter = monthLabelFormatter(resolveLocale(this.element, this.localeValue));
|
|
446
479
|
this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));
|
|
447
480
|
}
|
|
448
481
|
const [rangeStart, rangeEnd] = this.#visualRange();
|
|
@@ -48,6 +48,26 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
// src/utils/declared_value.ts
|
|
52
|
+
function parseDeclared(raw, parse, fallback) {
|
|
53
|
+
try {
|
|
54
|
+
return parse(raw);
|
|
55
|
+
} catch {
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function validSelector(element, raw, fallback) {
|
|
60
|
+
if (raw.length === 0) return fallback;
|
|
61
|
+
return parseDeclared(
|
|
62
|
+
raw,
|
|
63
|
+
(selector) => {
|
|
64
|
+
element.matches(selector);
|
|
65
|
+
return selector;
|
|
66
|
+
},
|
|
67
|
+
fallback
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
// src/utils/safe_timeout.ts
|
|
52
72
|
var TimerRegistry = class {
|
|
53
73
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -156,16 +176,7 @@ var DirectUploadController = class extends Controller {
|
|
|
156
176
|
}
|
|
157
177
|
/** Validates `scope` once so the per-event path never parses or throws. */
|
|
158
178
|
scopeValueChanged() {
|
|
159
|
-
|
|
160
|
-
if (selector.length > 0) {
|
|
161
|
-
try {
|
|
162
|
-
this.element.matches(selector);
|
|
163
|
-
this.#scopeSelector = selector;
|
|
164
|
-
return;
|
|
165
|
-
} catch {
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
this.#scopeSelector = "";
|
|
179
|
+
this.#scopeSelector = validSelector(this.element, this.scopeValue, "");
|
|
169
180
|
}
|
|
170
181
|
/** Updates a row's progress and the aggregate, emitting `progress`. */
|
|
171
182
|
#updateProgress(id, percent, name) {
|
|
@@ -2,6 +2,30 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/dirty_form_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/listener_set.ts
|
|
6
|
+
var ListenerSet = class {
|
|
7
|
+
/** The generation every `add` joins until the next `dispose()`. */
|
|
8
|
+
#abort = new AbortController();
|
|
9
|
+
/**
|
|
10
|
+
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
11
|
+
*
|
|
12
|
+
* The set supplies the signal, so `options` carries everything else the DOM
|
|
13
|
+
* accepts — `capture` included, which has to match at release time and no
|
|
14
|
+
* longer has a second place to drift from.
|
|
15
|
+
*/
|
|
16
|
+
add(target, type, handler, options) {
|
|
17
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Releases every listener of the open generation, synchronously, and opens the
|
|
21
|
+
* next one. Idempotent, and safe before anything has been added.
|
|
22
|
+
*/
|
|
23
|
+
dispose() {
|
|
24
|
+
this.#abort.abort();
|
|
25
|
+
this.#abort = new AbortController();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
5
29
|
// src/utils/safe_timeout.ts
|
|
6
30
|
var TimerRegistry = class {
|
|
7
31
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -125,6 +149,7 @@ var DirtyFormController = class extends Controller {
|
|
|
125
149
|
#dirty = false;
|
|
126
150
|
#beforeunloadBound = false;
|
|
127
151
|
#timeouts = new SafeTimeout();
|
|
152
|
+
#listeners = new ListenerSet();
|
|
128
153
|
/** Whether the active submission still represents the form's current values. */
|
|
129
154
|
#submitting = false;
|
|
130
155
|
#submitAttempt = null;
|
|
@@ -182,24 +207,19 @@ var DirtyFormController = class extends Controller {
|
|
|
182
207
|
this.#baseline = this.#serialize();
|
|
183
208
|
this.#setDirty(false);
|
|
184
209
|
this.element.removeAttribute("data-dirty");
|
|
185
|
-
this.element
|
|
186
|
-
this.element
|
|
187
|
-
this.element
|
|
188
|
-
this.element
|
|
189
|
-
this.element
|
|
190
|
-
this.element
|
|
210
|
+
this.#listeners.add(this.element, "input", this.#onFieldChange);
|
|
211
|
+
this.#listeners.add(this.element, "change", this.#onFieldChange);
|
|
212
|
+
this.#listeners.add(this.element, "submit", this.#onSubmit);
|
|
213
|
+
this.#listeners.add(this.element, "formdata", this.#onFormData);
|
|
214
|
+
this.#listeners.add(this.element, "turbo:submit-start", this.#onSubmitStart);
|
|
215
|
+
this.#listeners.add(this.element, "turbo:submit-end", this.#onSubmitEnd);
|
|
191
216
|
this.#unregisterVisit = registerVisitParticipant(this.element.ownerDocument, {
|
|
192
217
|
form: this.element,
|
|
193
218
|
evaluate: (event) => this.#evaluateVisit(event)
|
|
194
219
|
});
|
|
195
220
|
}
|
|
196
221
|
disconnect() {
|
|
197
|
-
this.
|
|
198
|
-
this.element.removeEventListener("change", this.#onFieldChange);
|
|
199
|
-
this.element.removeEventListener("submit", this.#onSubmit);
|
|
200
|
-
this.element.removeEventListener("formdata", this.#onFormData);
|
|
201
|
-
this.element.removeEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
202
|
-
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
222
|
+
this.#listeners.dispose();
|
|
203
223
|
this.#unregisterVisit?.();
|
|
204
224
|
this.#unregisterVisit = null;
|
|
205
225
|
this.#unbindBeforeUnload();
|