stimeo-ui 0.16.0 → 0.17.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 +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/input_mask_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/composition_tracker.ts
|
|
6
3
|
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
7
4
|
"insertCompositionText",
|
|
8
5
|
"insertFromComposition",
|
|
@@ -14,13 +11,11 @@ var CompositionTracker = class {
|
|
|
14
11
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
15
12
|
#onStart;
|
|
16
13
|
#onEnd;
|
|
17
|
-
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
18
14
|
#confirmedTarget = null;
|
|
19
15
|
constructor(options = {}) {
|
|
20
16
|
this.#onStart = options.onStart;
|
|
21
17
|
this.#onEnd = options.onEnd;
|
|
22
18
|
}
|
|
23
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
24
19
|
observe(target) {
|
|
25
20
|
if (this.#observedTargets.has(target)) return;
|
|
26
21
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
@@ -28,7 +23,6 @@ var CompositionTracker = class {
|
|
|
28
23
|
target.addEventListener("keydown", this.#handleKeydown);
|
|
29
24
|
this.#observedTargets.add(target);
|
|
30
25
|
}
|
|
31
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
32
26
|
unobserve(target) {
|
|
33
27
|
if (!this.#observedTargets.delete(target)) return;
|
|
34
28
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
@@ -37,7 +31,6 @@ var CompositionTracker = class {
|
|
|
37
31
|
this.#activeTargets.delete(target);
|
|
38
32
|
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
39
33
|
}
|
|
40
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
41
34
|
disconnect() {
|
|
42
35
|
for (const target of this.#observedTargets) {
|
|
43
36
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
@@ -48,16 +41,9 @@ var CompositionTracker = class {
|
|
|
48
41
|
this.#activeTargets.clear();
|
|
49
42
|
this.#confirmedTarget = null;
|
|
50
43
|
}
|
|
51
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
52
44
|
isComposing(event) {
|
|
53
45
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
54
46
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
57
|
-
*
|
|
58
|
-
* Asking closes the window either way, so one confirmation is folded at most
|
|
59
|
-
* once and a consumer asks once per `input`.
|
|
60
|
-
*/
|
|
61
47
|
consumesConfirmedInput(event) {
|
|
62
48
|
const confirmed = this.#confirmedTarget;
|
|
63
49
|
this.#confirmedTarget = null;
|
|
@@ -75,13 +61,11 @@ var CompositionTracker = class {
|
|
|
75
61
|
this.#confirmedTarget = event.target;
|
|
76
62
|
this.#onEnd?.(event);
|
|
77
63
|
};
|
|
78
|
-
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
79
64
|
#handleKeydown = () => {
|
|
80
65
|
this.#confirmedTarget = null;
|
|
81
66
|
};
|
|
82
67
|
};
|
|
83
68
|
|
|
84
|
-
// src/utils/declared_value.ts
|
|
85
69
|
function parseDeclared(raw, parse, fallback) {
|
|
86
70
|
try {
|
|
87
71
|
return parse(raw);
|
|
@@ -105,7 +89,6 @@ function parseJsonObject(raw) {
|
|
|
105
89
|
return parsed;
|
|
106
90
|
}
|
|
107
91
|
|
|
108
|
-
// src/utils/focus_candidate.ts
|
|
109
92
|
function inheritsFieldsetDisabled(control) {
|
|
110
93
|
let fieldset = control.closest("fieldset[disabled]");
|
|
111
94
|
while (fieldset) {
|
|
@@ -116,7 +99,6 @@ function inheritsFieldsetDisabled(control) {
|
|
|
116
99
|
return false;
|
|
117
100
|
}
|
|
118
101
|
|
|
119
|
-
// src/utils/half_width.ts
|
|
120
102
|
var FULL_WIDTH_SHIFT = 65248;
|
|
121
103
|
function halfWidthChar(char) {
|
|
122
104
|
if (char >= "\uFF01" && char <= "\uFF5E") {
|
|
@@ -125,7 +107,70 @@ function halfWidthChar(char) {
|
|
|
125
107
|
return char === "\u3000" ? " " : char;
|
|
126
108
|
}
|
|
127
109
|
|
|
128
|
-
|
|
110
|
+
var ListenerSet = class {
|
|
111
|
+
#abort = new AbortController();
|
|
112
|
+
add(target, type, handler, options) {
|
|
113
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
114
|
+
}
|
|
115
|
+
dispose() {
|
|
116
|
+
this.#abort.abort();
|
|
117
|
+
this.#abort = new AbortController();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
var MicrotaskCoalescer = class {
|
|
122
|
+
#run;
|
|
123
|
+
#queued = false;
|
|
124
|
+
#active = false;
|
|
125
|
+
#generation = 0;
|
|
126
|
+
constructor(run) {
|
|
127
|
+
this.#run = run;
|
|
128
|
+
}
|
|
129
|
+
activate() {
|
|
130
|
+
this.#active = true;
|
|
131
|
+
}
|
|
132
|
+
cancel() {
|
|
133
|
+
this.#active = false;
|
|
134
|
+
this.#queued = false;
|
|
135
|
+
this.#generation += 1;
|
|
136
|
+
}
|
|
137
|
+
schedule() {
|
|
138
|
+
if (!this.#active || this.#queued) return;
|
|
139
|
+
this.#queued = true;
|
|
140
|
+
const generation = this.#generation;
|
|
141
|
+
queueMicrotask(() => {
|
|
142
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
143
|
+
this.#queued = false;
|
|
144
|
+
this.#run();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
var MorphRenderWatcher = class {
|
|
150
|
+
#listeners = new ListenerSet();
|
|
151
|
+
#pass;
|
|
152
|
+
#element = null;
|
|
153
|
+
#onMorph = () => this.#pass.schedule();
|
|
154
|
+
constructor(run) {
|
|
155
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
156
|
+
}
|
|
157
|
+
observe(element) {
|
|
158
|
+
if (this.#element === element) return;
|
|
159
|
+
this.disconnect();
|
|
160
|
+
this.#element = element;
|
|
161
|
+
this.#pass.activate();
|
|
162
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
163
|
+
}
|
|
164
|
+
schedule() {
|
|
165
|
+
this.#pass.schedule();
|
|
166
|
+
}
|
|
167
|
+
disconnect() {
|
|
168
|
+
this.#listeners.dispose();
|
|
169
|
+
this.#pass.cancel();
|
|
170
|
+
this.#element = null;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
129
174
|
var DEFAULT_TOKENS = {
|
|
130
175
|
"9": "\\d",
|
|
131
176
|
a: "[A-Za-z]",
|
|
@@ -208,7 +253,6 @@ function tokenCharOffset(tokenFlags, n) {
|
|
|
208
253
|
return -1;
|
|
209
254
|
}
|
|
210
255
|
var InputMaskController = class extends Controller {
|
|
211
|
-
/** The attribute above, in the namespace this controller is registered under. */
|
|
212
256
|
get #unmaskAttribute() {
|
|
213
257
|
return `data-${this.identifier}-${UNMASK_ATTR}`;
|
|
214
258
|
}
|
|
@@ -219,19 +263,10 @@ var InputMaskController = class extends Controller {
|
|
|
219
263
|
};
|
|
220
264
|
static actions = ["format"];
|
|
221
265
|
static events = ["change", "reconcile"];
|
|
222
|
-
/** The value this controller last committed, and the baseline both events compare. */
|
|
223
266
|
#lastValue = null;
|
|
224
267
|
#started = false;
|
|
225
|
-
/** Validated token map; the hot path never parses the `tokens` declaration. */
|
|
226
268
|
#tokens = compileTokens("");
|
|
227
|
-
/** The field's text when the running composition began. */
|
|
228
269
|
#textBeforeComposition = "";
|
|
229
|
-
/**
|
|
230
|
-
* Holds mid-composition input so the IME's uncommitted text is never rewritten,
|
|
231
|
-
* and formats the confirmed text once when the composition ends. A composition
|
|
232
|
-
* that ends on the text it began with committed nothing, so a re-format then is
|
|
233
|
-
* the page's: the mask changed while it ran.
|
|
234
|
-
*/
|
|
235
270
|
#composition = new CompositionTracker({
|
|
236
271
|
onStart: () => {
|
|
237
272
|
this.#textBeforeComposition = this.element.value;
|
|
@@ -241,7 +276,6 @@ var InputMaskController = class extends Controller {
|
|
|
241
276
|
this.#reformat(committedNothing ? "reconcile" : "edit");
|
|
242
277
|
}
|
|
243
278
|
});
|
|
244
|
-
/** Re-parses the declaration and re-formats under the tokens it now selects. */
|
|
245
279
|
tokensValueChanged() {
|
|
246
280
|
this.#tokens = compileTokens(this.tokensValue);
|
|
247
281
|
if (this.#started) this.#reformat("reconcile");
|
|
@@ -249,7 +283,6 @@ var InputMaskController = class extends Controller {
|
|
|
249
283
|
patternValueChanged() {
|
|
250
284
|
if (this.#started) this.#reformat("reconcile");
|
|
251
285
|
}
|
|
252
|
-
/** Clears a sink it stops maintaining so a submit cannot carry a stale raw value. */
|
|
253
286
|
unmaskToHiddenValueChanged() {
|
|
254
287
|
if (!this.#started) return;
|
|
255
288
|
if (!this.unmaskToHiddenValue) {
|
|
@@ -258,27 +291,23 @@ var InputMaskController = class extends Controller {
|
|
|
258
291
|
}
|
|
259
292
|
this.#reformat("reconcile");
|
|
260
293
|
}
|
|
294
|
+
#morph = new MorphRenderWatcher(() => this.#reformat("reconcile"));
|
|
261
295
|
connect() {
|
|
296
|
+
this.#morph.observe(this.element);
|
|
262
297
|
this.#started = true;
|
|
263
298
|
this.#composition.observe(this.element);
|
|
264
299
|
this.#lastValue = this.element.value;
|
|
265
300
|
this.#reformat("reconcile");
|
|
266
301
|
}
|
|
267
302
|
disconnect() {
|
|
303
|
+
this.#morph.disconnect();
|
|
268
304
|
this.#started = false;
|
|
269
305
|
this.#composition.disconnect();
|
|
270
306
|
}
|
|
271
|
-
/** Formats the field on input, preserving the caret. Bound via `data-action`. */
|
|
272
307
|
format(event) {
|
|
273
308
|
if (this.#composition.isComposing(event)) return;
|
|
274
309
|
this.#reformat("edit", deletionOf(event));
|
|
275
310
|
}
|
|
276
|
-
/**
|
|
277
|
-
* Core reformat: mask the current value, restore the caret, sync the sink and
|
|
278
|
-
* the state hooks, and report a moved value under the event `cause` selects.
|
|
279
|
-
*
|
|
280
|
-
* @stimeoRenderRoot
|
|
281
|
-
*/
|
|
282
311
|
#reformat(cause, deletion = null) {
|
|
283
312
|
if (this.#composition.isComposing()) return;
|
|
284
313
|
const input = this.element;
|
|
@@ -309,8 +338,10 @@ var InputMaskController = class extends Controller {
|
|
|
309
338
|
this.#lastValue = raw;
|
|
310
339
|
return;
|
|
311
340
|
}
|
|
312
|
-
input.value
|
|
313
|
-
|
|
341
|
+
if (input.value !== result.masked) {
|
|
342
|
+
input.value = result.masked;
|
|
343
|
+
this.#restoreCaret(result.tokenFlags, anchor);
|
|
344
|
+
}
|
|
314
345
|
this.#publish(result, result.masked);
|
|
315
346
|
if (result.masked === this.#lastValue) return;
|
|
316
347
|
this.#lastValue = result.masked;
|
|
@@ -318,7 +349,6 @@ var InputMaskController = class extends Controller {
|
|
|
318
349
|
if (cause === "edit") this.dispatch("change", { detail });
|
|
319
350
|
else this.dispatch("reconcile", { detail });
|
|
320
351
|
}
|
|
321
|
-
/** Syncs the raw-value sink and the state hooks for the `shown` field text. */
|
|
322
352
|
#publish(result, shown) {
|
|
323
353
|
if (this.unmaskToHiddenValue) {
|
|
324
354
|
const sink = this.#resolveSink();
|
|
@@ -327,15 +357,9 @@ var InputMaskController = class extends Controller {
|
|
|
327
357
|
this.#flag("data-mask-complete", result.complete);
|
|
328
358
|
this.#flag("data-mask-empty", shown.length === 0);
|
|
329
359
|
}
|
|
330
|
-
/**
|
|
331
|
-
* How many token slots the text before the caret fills. Masking that prefix is
|
|
332
|
-
* what makes a rejected character — or a literal that also matches a token —
|
|
333
|
-
* count exactly as the rendering counts it, so the caret cannot drift.
|
|
334
|
-
*/
|
|
335
360
|
#significantBefore(prefix) {
|
|
336
361
|
return applyMask(prefix, this.patternValue, this.#tokens).unmasked.length;
|
|
337
362
|
}
|
|
338
|
-
/** Places the caret after the `n`-th token char (skipping following literals). */
|
|
339
363
|
#restoreCaret(tokenFlags, n) {
|
|
340
364
|
let position;
|
|
341
365
|
if (n <= 0) {
|
|
@@ -357,23 +381,6 @@ var InputMaskController = class extends Controller {
|
|
|
357
381
|
} catch {
|
|
358
382
|
}
|
|
359
383
|
}
|
|
360
|
-
/**
|
|
361
|
-
* The hidden raw-value sink for this input, resolved so several masked inputs
|
|
362
|
-
* can coexist in one form:
|
|
363
|
-
*
|
|
364
|
-
* 1. **Explicit pairing** — a sink whose attribute value names this input's
|
|
365
|
-
* `id` (`data-stimeo--input-mask-unmask="zip"`).
|
|
366
|
-
* 2. **Nearest container** — otherwise, walking up from the input, the first
|
|
367
|
-
* *value-less* sink in the closest ancestor. Wrapped input+sink pairs each
|
|
368
|
-
* find their own sink, and a single form-level sink is found the same
|
|
369
|
-
* way. A sink claimed by another input's id is never matched here.
|
|
370
|
-
*
|
|
371
|
-
* Both steps only ever consider the associated form's own controls, so a sink
|
|
372
|
-
* belonging to a different form (or to none) is never written, while one the
|
|
373
|
-
* `form` attribute associates from elsewhere in the document still resolves —
|
|
374
|
-
* for either the input or the sink, neither of which needs to contain or be
|
|
375
|
-
* contained by the form. A form-less input reads the document instead.
|
|
376
|
-
*/
|
|
377
384
|
#resolveSink() {
|
|
378
385
|
const candidates = this.#sinkCandidates();
|
|
379
386
|
const id = this.element.id;
|
|
@@ -389,12 +396,6 @@ var InputMaskController = class extends Controller {
|
|
|
389
396
|
}
|
|
390
397
|
return null;
|
|
391
398
|
}
|
|
392
|
-
/**
|
|
393
|
-
* Every sink the input's form owns, in document order — one collection read per
|
|
394
|
-
* resolution, so the cost tracks the form's own controls and not the depth of
|
|
395
|
-
* the markup around it. `form.elements` lists controls the `form` attribute
|
|
396
|
-
* associates from anywhere in the document, and lists nothing another form owns.
|
|
397
|
-
*/
|
|
398
399
|
#sinkCandidates() {
|
|
399
400
|
const form = this.element.form;
|
|
400
401
|
if (form === null) {
|
|
@@ -406,7 +407,6 @@ var InputMaskController = class extends Controller {
|
|
|
406
407
|
(element) => element instanceof HTMLInputElement && element.hasAttribute(this.#unmaskAttribute)
|
|
407
408
|
);
|
|
408
409
|
}
|
|
409
|
-
/** Sets a boolean `data-*` flag to `"true"` when `on`, else removes it. */
|
|
410
410
|
#flag(name, on) {
|
|
411
411
|
if (on) this.element.setAttribute(name, "true");
|
|
412
412
|
else this.element.removeAttribute(name);
|
|
@@ -414,5 +414,4 @@ var InputMaskController = class extends Controller {
|
|
|
414
414
|
};
|
|
415
415
|
|
|
416
416
|
export { InputMaskController, applyMask };
|
|
417
|
-
//# sourceMappingURL=input_mask_controller.js.map
|
|
418
417
|
//# sourceMappingURL=input_mask_controller.js.map
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/intersection_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/declared_value.ts
|
|
6
3
|
function parseDeclared(raw, parse, fallback) {
|
|
7
4
|
try {
|
|
8
5
|
return parse(raw);
|
|
@@ -10,8 +7,18 @@ function parseDeclared(raw, parse, fallback) {
|
|
|
10
7
|
return fallback;
|
|
11
8
|
}
|
|
12
9
|
}
|
|
10
|
+
function validSelector(element, raw, fallback) {
|
|
11
|
+
if (raw.length === 0) return fallback;
|
|
12
|
+
return parseDeclared(
|
|
13
|
+
raw,
|
|
14
|
+
(selector) => {
|
|
15
|
+
element.matches(selector);
|
|
16
|
+
return selector;
|
|
17
|
+
},
|
|
18
|
+
fallback
|
|
19
|
+
);
|
|
20
|
+
}
|
|
13
21
|
|
|
14
|
-
// src/utils/intersection_watcher.ts
|
|
15
22
|
function isBeforeRootStart(entry) {
|
|
16
23
|
const rect = entry.boundingClientRect;
|
|
17
24
|
if (rect.width === 0 && rect.height === 0) return false;
|
|
@@ -30,30 +37,12 @@ var IntersectionWatcher = class {
|
|
|
30
37
|
constructor(onEntries) {
|
|
31
38
|
this.#onEntries = onEntries;
|
|
32
39
|
}
|
|
33
|
-
/** Whether an observer is live (started, `IntersectionObserver` supported). */
|
|
34
40
|
get active() {
|
|
35
41
|
return this.#active;
|
|
36
42
|
}
|
|
37
|
-
/** Whether the live observer discarded configured options after construction failed. */
|
|
38
43
|
get usingPlatformDefaults() {
|
|
39
44
|
return this.#usingPlatformDefaults;
|
|
40
45
|
}
|
|
41
|
-
/**
|
|
42
|
-
* (Re)creates the observer and observes `targets`. Returns `false` — leaving
|
|
43
|
-
* the watcher inert — without `IntersectionObserver` support (very old
|
|
44
|
-
* browsers; the caller's no-JS fallback stays in charge) or with no targets.
|
|
45
|
-
* If initial construction with the configured options fails, the watcher
|
|
46
|
-
* warns and retries once with the same root and platform defaults. A
|
|
47
|
-
* `rootSelector` that does not parse resolves to the viewport (see
|
|
48
|
-
* {@link IntersectionWatchOptions.rootSelector}), so a typo never fails the
|
|
49
|
-
* call.
|
|
50
|
-
*
|
|
51
|
-
* @throws The fallback constructor error if both construction attempts fail,
|
|
52
|
-
* or whatever the platform throws from `observe()`. The exception is passed
|
|
53
|
-
* through unchanged, but the watcher rolls back first: every target observed
|
|
54
|
-
* so far is released and `active` stays `false`, so a caller that retries
|
|
55
|
-
* starts from a clean slate.
|
|
56
|
-
*/
|
|
57
46
|
start(targets, options = {}) {
|
|
58
47
|
this.stop();
|
|
59
48
|
if (typeof IntersectionObserver === "undefined") return false;
|
|
@@ -91,14 +80,6 @@ var IntersectionWatcher = class {
|
|
|
91
80
|
throw error;
|
|
92
81
|
}
|
|
93
82
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Re-delivers `target`'s CURRENT intersection state: `IntersectionObserver`
|
|
96
|
-
* only reports *changes*, but `observe()` always reports the present state,
|
|
97
|
-
* so unobserve→observe turns "still intersecting" into a fresh callback.
|
|
98
|
-
*
|
|
99
|
-
* @throws Whatever `unobserve()`/`observe()` throws. The watcher is stopped
|
|
100
|
-
* first, so it never stays live with a half-rearmed target.
|
|
101
|
-
*/
|
|
102
83
|
rearm(target) {
|
|
103
84
|
if (!this.#observer) return;
|
|
104
85
|
try {
|
|
@@ -109,7 +90,6 @@ var IntersectionWatcher = class {
|
|
|
109
90
|
throw error;
|
|
110
91
|
}
|
|
111
92
|
}
|
|
112
|
-
/** Severs the observer; late queued callbacks become no-ops via the guard. */
|
|
113
93
|
stop() {
|
|
114
94
|
this.#active = false;
|
|
115
95
|
this.#observer?.disconnect();
|
|
@@ -118,10 +98,82 @@ var IntersectionWatcher = class {
|
|
|
118
98
|
}
|
|
119
99
|
};
|
|
120
100
|
|
|
121
|
-
|
|
101
|
+
var MicrotaskCoalescer = class {
|
|
102
|
+
#run;
|
|
103
|
+
#queued = false;
|
|
104
|
+
#active = false;
|
|
105
|
+
#generation = 0;
|
|
106
|
+
constructor(run) {
|
|
107
|
+
this.#run = run;
|
|
108
|
+
}
|
|
109
|
+
activate() {
|
|
110
|
+
this.#active = true;
|
|
111
|
+
}
|
|
112
|
+
cancel() {
|
|
113
|
+
this.#active = false;
|
|
114
|
+
this.#queued = false;
|
|
115
|
+
this.#generation += 1;
|
|
116
|
+
}
|
|
117
|
+
schedule() {
|
|
118
|
+
if (!this.#active || this.#queued) return;
|
|
119
|
+
this.#queued = true;
|
|
120
|
+
const generation = this.#generation;
|
|
121
|
+
queueMicrotask(() => {
|
|
122
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
123
|
+
this.#queued = false;
|
|
124
|
+
this.#run();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
var NUMBER_BOUNDS = {
|
|
130
|
+
finite: { finite: true }};
|
|
131
|
+
function matchesNumberBounds(value, bounds) {
|
|
132
|
+
if (!Number.isFinite(value)) {
|
|
133
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
134
|
+
if (direction === null) return false;
|
|
135
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
136
|
+
}
|
|
137
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
138
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
139
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
140
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
141
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readNumber(raw, fallback, bounds) {
|
|
146
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
var NumberValueReader = class {
|
|
150
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
151
|
+
read(owner, name, raw, fallback, bounds) {
|
|
152
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
153
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
154
|
+
this.#lastRejected.delete(name);
|
|
155
|
+
return resolved;
|
|
156
|
+
}
|
|
157
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
158
|
+
const literal = owner.element.getAttribute(attribute);
|
|
159
|
+
if (literal === null) {
|
|
160
|
+
this.#lastRejected.delete(name);
|
|
161
|
+
return resolved;
|
|
162
|
+
}
|
|
163
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
164
|
+
this.#lastRejected.set(name, literal);
|
|
165
|
+
console.warn(
|
|
166
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return resolved;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
122
173
|
var RATIO_PROPERTY = "--stimeo--intersection-ratio";
|
|
123
174
|
var RATIO_EPSILON = 0.01;
|
|
124
|
-
var IntersectionController = class extends Controller {
|
|
175
|
+
var IntersectionController = class _IntersectionController extends Controller {
|
|
176
|
+
#numbers = new NumberValueReader();
|
|
125
177
|
static values = {
|
|
126
178
|
threshold: { type: Number, default: 0 },
|
|
127
179
|
ratioSteps: { type: Number, default: 0 },
|
|
@@ -129,13 +181,18 @@ var IntersectionController = class extends Controller {
|
|
|
129
181
|
rootSelector: { type: String, default: "" },
|
|
130
182
|
once: { type: Boolean, default: false }
|
|
131
183
|
};
|
|
184
|
+
static valueConstraints = {
|
|
185
|
+
threshold: NUMBER_BOUNDS.finite,
|
|
186
|
+
ratioSteps: { finite: true, min: 0, max: 1e3 }
|
|
187
|
+
};
|
|
132
188
|
static actions = ["refresh"];
|
|
133
189
|
static events = ["enter", "exit", "change", "passed"];
|
|
134
|
-
/** Shared IO plumbing (support guard, root resolution, active guard, re-arm). */
|
|
135
190
|
#watcher = new IntersectionWatcher((entries) => this.#onIntersect(entries));
|
|
136
|
-
|
|
191
|
+
#rebuild = new MicrotaskCoalescer(() => this.#sync());
|
|
192
|
+
#rootSelector = "";
|
|
193
|
+
#builtRoot = null;
|
|
194
|
+
#builtOptions = "";
|
|
137
195
|
#effectiveThreshold = 0;
|
|
138
|
-
/** Bumped by `refresh()`: an in-flight batch becomes stale and stops. */
|
|
139
196
|
#generation = 0;
|
|
140
197
|
#onIntersect(entries) {
|
|
141
198
|
const generation = this.#generation;
|
|
@@ -151,46 +208,49 @@ var IntersectionController = class extends Controller {
|
|
|
151
208
|
}
|
|
152
209
|
}
|
|
153
210
|
connect() {
|
|
154
|
-
|
|
155
|
-
this.#
|
|
211
|
+
this.#rebuild.activate();
|
|
212
|
+
this.#sync();
|
|
156
213
|
}
|
|
157
214
|
disconnect() {
|
|
215
|
+
this.#rebuild.cancel();
|
|
158
216
|
this.#watcher.stop();
|
|
159
217
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Re-reads the visibility line and rebuilds the observer. Turbo 8 morphing
|
|
162
|
-
* rewrites the attribute in place without a reconnect, and the line is what
|
|
163
|
-
* the intersection callback compares every ratio against, so a value frozen at
|
|
164
|
-
* connect time would decide `data-intersecting` wrongly for the rest of the
|
|
165
|
-
* page's life. Nothing to rebuild before the first `connect()`; after a spent
|
|
166
|
-
* one-shot the watcher is deliberately stopped, and re-observing would deliver
|
|
167
|
-
* the current state and fire `enter` a second time.
|
|
168
|
-
*/
|
|
169
218
|
thresholdValueChanged() {
|
|
170
|
-
|
|
219
|
+
this.#rebuild.schedule();
|
|
220
|
+
}
|
|
221
|
+
ratioStepsValueChanged() {
|
|
222
|
+
this.#rebuild.schedule();
|
|
223
|
+
}
|
|
224
|
+
rootMarginValueChanged() {
|
|
225
|
+
this.#rebuild.schedule();
|
|
226
|
+
}
|
|
227
|
+
rootSelectorValueChanged() {
|
|
228
|
+
this.#rootSelector = validSelector(this.element, this.rootSelectorValue, "");
|
|
229
|
+
this.#rebuild.schedule();
|
|
171
230
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
231
|
+
onceValueChanged() {
|
|
232
|
+
this.#rebuild.schedule();
|
|
233
|
+
}
|
|
234
|
+
#sync() {
|
|
235
|
+
if (this.onceValue && this.element.getAttribute("data-intersecting") === "true") {
|
|
236
|
+
this.#watcher.stop();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const root = this.#rootSelector ? document.querySelector(this.#rootSelector) : null;
|
|
240
|
+
const threshold = this.#clampedThreshold();
|
|
241
|
+
const thresholds = this.#thresholds();
|
|
242
|
+
const options = `${this.rootMarginValue} ${threshold} ${thresholds.join(",")}`;
|
|
243
|
+
if (this.#watcher.active && root === this.#builtRoot && options === this.#builtOptions) return;
|
|
244
|
+
this.#builtRoot = root;
|
|
245
|
+
this.#builtOptions = options;
|
|
246
|
+
this.#effectiveThreshold = threshold;
|
|
175
247
|
this.#watcher.start(this.element, {
|
|
176
|
-
|
|
248
|
+
root,
|
|
177
249
|
rootMargin: this.rootMarginValue,
|
|
178
|
-
threshold:
|
|
250
|
+
threshold: thresholds
|
|
179
251
|
});
|
|
180
252
|
if (this.#watcher.usingPlatformDefaults) this.#effectiveThreshold = 0;
|
|
181
253
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Re-delivers the current intersection state as a fresh transition. Bound via
|
|
184
|
-
* `data-action` (e.g. `my-feed:appended@window->stimeo--intersection#refresh`).
|
|
185
|
-
*
|
|
186
|
-
* `IntersectionObserver` only reports state *changes*, so a sentinel that
|
|
187
|
-
* stays visible while content is appended below it never fires `enter` again
|
|
188
|
-
* and a hand-rolled infinite scroll stalls. `observe()` always delivers the
|
|
189
|
-
* current state, and clearing the recorded `data-intersecting`/`data-passed`
|
|
190
|
-
* makes that delivery count as a transition — a still-visible sentinel
|
|
191
|
-
* re-fires `enter`. No-op once the observer is gone (`once` fired, no
|
|
192
|
-
* `IntersectionObserver` support, or after `disconnect()`).
|
|
193
|
-
*/
|
|
194
254
|
refresh() {
|
|
195
255
|
if (!this.#watcher.active) return;
|
|
196
256
|
this.#generation += 1;
|
|
@@ -198,16 +258,6 @@ var IntersectionController = class extends Controller {
|
|
|
198
258
|
this.element.removeAttribute("data-passed");
|
|
199
259
|
this.#watcher.rearm(this.element);
|
|
200
260
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Reflects the visibility onto `data-intersecting` and fires `enter`/`exit`
|
|
203
|
-
* on transitions. The previous state is the DOM attribute (source of truth),
|
|
204
|
-
* so the observer's initial callback fires `enter` for an element that starts
|
|
205
|
-
* visible but stays silent after a cache restore that already recorded it.
|
|
206
|
-
* An initial not-visible state is established silently (no `exit`).
|
|
207
|
-
*
|
|
208
|
-
* @stimeoRuntimeOnly `once` decides whether this enter spends the watcher's one shot; the hook it
|
|
209
|
-
* writes follows the entry.
|
|
210
|
-
*/
|
|
211
261
|
#syncIntersecting(intersecting, ratio, entry) {
|
|
212
262
|
const previous = this.element.getAttribute("data-intersecting");
|
|
213
263
|
this.element.setAttribute("data-intersecting", intersecting ? "true" : "false");
|
|
@@ -220,57 +270,48 @@ var IntersectionController = class extends Controller {
|
|
|
220
270
|
});
|
|
221
271
|
}
|
|
222
272
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Which edge the element left across, for the `exit` detail. A non-zero
|
|
225
|
-
* `threshold` withdraws visibility while the element still overlaps the root,
|
|
226
|
-
* so the leaving rect can straddle the start edge — the direction is the
|
|
227
|
-
* element's own top against that edge, not whether it has cleared the root
|
|
228
|
-
* entirely (that is what `passed` reports). An element with no layout box
|
|
229
|
-
* (`display: none`, a collapsed `<details>`) is reported with an empty rect
|
|
230
|
-
* that carries no position at all, so it is deliberately neither direction
|
|
231
|
-
* and takes the "still ahead" reading.
|
|
232
|
-
*/
|
|
233
273
|
#leftViaStartEdge(entry) {
|
|
234
274
|
const rect = entry.boundingClientRect;
|
|
235
275
|
if (rect.width === 0 && rect.height === 0) return false;
|
|
236
276
|
return rect.top < (entry.rootBounds?.top ?? 0);
|
|
237
277
|
}
|
|
238
|
-
/**
|
|
239
|
-
* Reflects the "scrolled past" state onto `data-passed` and fires `passed` on
|
|
240
|
-
* transitions — the line sticky headers and reading progress key off. Like
|
|
241
|
-
* `enter`, an initial `passed=true` (page restored mid-scroll) fires; the
|
|
242
|
-
* initial `false` is established silently.
|
|
243
|
-
*/
|
|
244
278
|
#syncPassed(passed) {
|
|
245
279
|
const previous = this.element.getAttribute("data-passed");
|
|
246
280
|
this.element.setAttribute("data-passed", passed ? "true" : "false");
|
|
247
281
|
const changed = previous === null ? passed : previous === "true" !== passed;
|
|
248
282
|
if (changed) this.dispatch("passed", { detail: { passed } });
|
|
249
283
|
}
|
|
250
|
-
/** The configured `threshold`, clamped to the 0..1 the observer accepts. */
|
|
251
284
|
#clampedThreshold() {
|
|
252
|
-
return Math.min(1, Math.max(0, this
|
|
285
|
+
return Math.min(1, Math.max(0, this.#safeThreshold));
|
|
253
286
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Observer thresholds: the `threshold` line itself, plus `ratioSteps` evenly
|
|
256
|
-
* spaced steps when fine-grained `change` ratios are wanted (progress bars).
|
|
257
|
-
*
|
|
258
|
-
* 0 is always observed. An observer notifies only at the lines it was given,
|
|
259
|
-
* so a non-zero `threshold` on its own delivers its last callback while the
|
|
260
|
-
* element is still partly visible: the element leaving for good would never be
|
|
261
|
-
* reported, freezing the ratio and `data-passed` mid-departure.
|
|
262
|
-
*/
|
|
263
287
|
#thresholds() {
|
|
264
288
|
const thresholds = /* @__PURE__ */ new Set([0, this.#clampedThreshold()]);
|
|
265
|
-
if (this
|
|
266
|
-
for (let i = 0; i <= this
|
|
267
|
-
thresholds.add(i / this
|
|
289
|
+
if (this.#safeRatioSteps > 0) {
|
|
290
|
+
for (let i = 0; i <= this.#safeRatioSteps; i += 1) {
|
|
291
|
+
thresholds.add(i / this.#safeRatioSteps);
|
|
268
292
|
}
|
|
269
293
|
}
|
|
270
294
|
return [...thresholds].sort((a, b) => a - b);
|
|
271
295
|
}
|
|
296
|
+
get #safeThreshold() {
|
|
297
|
+
return this.#numbers.read(
|
|
298
|
+
this,
|
|
299
|
+
"threshold",
|
|
300
|
+
this.thresholdValue,
|
|
301
|
+
_IntersectionController.values.threshold.default,
|
|
302
|
+
_IntersectionController.valueConstraints.threshold
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
get #safeRatioSteps() {
|
|
306
|
+
return this.#numbers.read(
|
|
307
|
+
this,
|
|
308
|
+
"ratioSteps",
|
|
309
|
+
this.ratioStepsValue,
|
|
310
|
+
_IntersectionController.values.ratioSteps.default,
|
|
311
|
+
_IntersectionController.valueConstraints.ratioSteps
|
|
312
|
+
);
|
|
313
|
+
}
|
|
272
314
|
};
|
|
273
315
|
|
|
274
316
|
export { IntersectionController };
|
|
275
|
-
//# sourceMappingURL=intersection_controller.js.map
|
|
276
317
|
//# sourceMappingURL=intersection_controller.js.map
|