stimeo-ui 0.15.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 +209 -0
- data/README.md +128 -1
- data/dist/cable/index.js +392 -417
- data/dist/controllers/accordion_controller.js +228 -28
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -191
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +295 -110
- data/dist/controllers/avatar_controller.js +172 -106
- data/dist/controllers/breadcrumb_controller.js +0 -129
- data/dist/controllers/bulk_select_controller.js +264 -53
- data/dist/controllers/calendar_controller.js +346 -180
- data/dist/controllers/carousel_controller.js +587 -300
- data/dist/controllers/character_counter_controller.js +271 -143
- data/dist/controllers/checkbox_controller.js +77 -75
- data/dist/controllers/clipboard_controller.js +187 -108
- data/dist/controllers/collapsible_controller.js +374 -108
- data/dist/controllers/color_picker_controller.js +183 -127
- data/dist/controllers/combobox_controller.js +426 -109
- data/dist/controllers/command_palette_controller.js +994 -350
- data/dist/controllers/conditional_fields_controller.js +212 -125
- data/dist/controllers/confirm_controller.js +939 -245
- data/dist/controllers/context_menu_controller.js +275 -127
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +235 -97
- data/dist/controllers/currency_input_controller.js +241 -118
- data/dist/controllers/data_grid_controller.js +229 -135
- data/dist/controllers/date_range_picker_controller.js +359 -165
- data/dist/controllers/dialog_controller.js +887 -225
- data/dist/controllers/direct_upload_controller.js +80 -142
- data/dist/controllers/dirty_form_controller.js +26 -47
- data/dist/controllers/dismissible_controller.js +175 -20
- data/dist/controllers/drawer_controller.js +963 -336
- data/dist/controllers/dropdown_controller.js +342 -85
- data/dist/controllers/editable_controller.js +182 -58
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +426 -229
- data/dist/controllers/filter_controller.js +78 -36
- data/dist/controllers/flash_controller.js +328 -211
- data/dist/controllers/focus_controller.js +654 -239
- data/dist/controllers/form_field_controller.js +153 -136
- data/dist/controllers/form_validation_controller.js +29 -96
- data/dist/controllers/frame_loading_controller.js +263 -198
- data/dist/controllers/highlight_controller.js +119 -70
- data/dist/controllers/hover_card_controller.js +335 -118
- data/dist/controllers/idle_controller.js +316 -65
- data/dist/controllers/input_mask_controller.js +116 -61
- data/dist/controllers/intersection_controller.js +147 -103
- data/dist/controllers/lazy_frame_controller.js +9 -56
- data/dist/controllers/listbox_controller.js +413 -153
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -95
- data/dist/controllers/menu_controller.js +254 -167
- data/dist/controllers/menubar_controller.js +206 -331
- data/dist/controllers/meter_controller.js +159 -71
- data/dist/controllers/multi_select_controller.js +797 -388
- data/dist/controllers/navigation_menu_controller.js +156 -231
- data/dist/controllers/nested_form_controller.js +373 -174
- data/dist/controllers/network_status_controller.js +241 -45
- data/dist/controllers/number_input_controller.js +387 -227
- data/dist/controllers/optimistic_controller.js +28 -75
- data/dist/controllers/otp_controller.js +344 -196
- data/dist/controllers/overflow_indicator_controller.js +167 -93
- data/dist/controllers/overflow_menu_controller.js +408 -277
- data/dist/controllers/pagination_controller.js +342 -120
- data/dist/controllers/password_reveal_controller.js +424 -99
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +252 -96
- data/dist/controllers/pointer_drag_controller.js +263 -164
- data/dist/controllers/popover_controller.js +267 -94
- data/dist/controllers/portal_controller.js +209 -72
- data/dist/controllers/preview_guard_controller.js +243 -112
- data/dist/controllers/progress_controller.js +132 -59
- data/dist/controllers/radio_group_controller.js +138 -133
- data/dist/controllers/range_slider_controller.js +245 -112
- data/dist/controllers/rating_controller.js +429 -194
- data/dist/controllers/read_more_controller.js +185 -47
- 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 +294 -118
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -221
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -146
- data/dist/controllers/scrollspy_controller.js +181 -236
- data/dist/controllers/separator_controller.js +288 -132
- data/dist/controllers/sidebar_controller.js +1071 -375
- data/dist/controllers/skeleton_controller.js +248 -92
- data/dist/controllers/slider_controller.js +211 -96
- data/dist/controllers/smart_sticky_header_controller.js +163 -54
- data/dist/controllers/sortable_controller.js +13 -110
- data/dist/controllers/spinner_controller.js +277 -161
- data/dist/controllers/step_indicator_controller.js +107 -53
- data/dist/controllers/stepper_controller.js +171 -48
- data/dist/controllers/stick_to_bottom_controller.js +310 -116
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -192
- data/dist/controllers/switch_controller.js +107 -43
- data/dist/controllers/tabs_controller.js +50 -25
- data/dist/controllers/tags_input_controller.js +290 -149
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +148 -80
- data/dist/controllers/time_picker_controller.js +162 -81
- data/dist/controllers/toast_controller.js +365 -193
- data/dist/controllers/toggle_group_controller.js +194 -99
- data/dist/controllers/toolbar_controller.js +92 -90
- data/dist/controllers/tooltip_controller.js +185 -125
- data/dist/controllers/transition_controller.js +123 -115
- data/dist/controllers/tree_view_controller.js +248 -290
- data/dist/index.js +8649 -8572
- data/dist/positioning/index.js +148 -45
- 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/auto_submit_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/announce.ts
|
|
6
3
|
function announce(message, options = {}) {
|
|
7
4
|
const text = message.trim();
|
|
8
5
|
if (text.length === 0) return;
|
|
@@ -13,143 +10,331 @@ function announce(message, options = {}) {
|
|
|
13
10
|
);
|
|
14
11
|
}
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
13
|
+
function sharedRegistry(key, create) {
|
|
14
|
+
const symbol = Symbol.for(key);
|
|
15
|
+
const scope = globalThis;
|
|
16
|
+
const existing = scope[symbol];
|
|
17
|
+
if (existing !== void 0) return existing;
|
|
18
|
+
const registry = create();
|
|
19
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
20
|
+
return registry;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isAttributeValue(value) {
|
|
24
|
+
return value === null || typeof value === "string";
|
|
25
|
+
}
|
|
26
|
+
function readLeaseRecord(element, record) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
29
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
30
|
+
const [original, written, ...rest] = parsed;
|
|
31
|
+
const linked = rest[0] !== false;
|
|
32
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
33
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
34
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
35
|
+
} catch {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
var AttributeLease = class _AttributeLease {
|
|
40
|
+
static #registry = sharedRegistry(
|
|
41
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
42
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
43
|
+
);
|
|
44
|
+
#attribute;
|
|
45
|
+
#record;
|
|
46
|
+
#base;
|
|
47
|
+
#held = /* @__PURE__ */ new Set();
|
|
48
|
+
constructor(attribute, owner, options = {}) {
|
|
49
|
+
this.#attribute = attribute;
|
|
50
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
51
|
+
this.#base = options.base === true;
|
|
52
|
+
const names = _AttributeLease.#registry.names;
|
|
53
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
54
|
+
}
|
|
55
|
+
write(element, value) {
|
|
56
|
+
const stack = this.#stack(element);
|
|
57
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
58
|
+
this.#held.add(element);
|
|
59
|
+
const shows = !this.#covered(element, stack, hold);
|
|
60
|
+
hold.written = value;
|
|
61
|
+
this.#mark(element, stack);
|
|
62
|
+
if (shows) this.#reflect(element, value);
|
|
63
|
+
}
|
|
64
|
+
return(element) {
|
|
65
|
+
const held = this.#held.delete(element);
|
|
66
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
67
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
68
|
+
const hold = this.#take(stack);
|
|
69
|
+
if (!hold) return false;
|
|
70
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
71
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
72
|
+
const index = stack.indexOf(hold);
|
|
73
|
+
stack.splice(index, 1);
|
|
74
|
+
const above = stack[index];
|
|
75
|
+
if (above) above.linked &&= hold.linked;
|
|
76
|
+
this.#mark(element, stack);
|
|
77
|
+
if (!owns) return true;
|
|
78
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
79
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
returnAll() {
|
|
83
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
84
|
+
}
|
|
85
|
+
#stack(element) {
|
|
86
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
87
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
88
|
+
stacks.set(element, byAttribute);
|
|
89
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
90
|
+
byAttribute.set(this.#attribute, stack);
|
|
91
|
+
return stack;
|
|
92
|
+
}
|
|
93
|
+
#seed(element) {
|
|
94
|
+
const found = [];
|
|
95
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
96
|
+
const read = readLeaseRecord(element, record);
|
|
97
|
+
if (read) found.push({ ...read, record });
|
|
35
98
|
}
|
|
99
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
100
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
101
|
+
lease: null,
|
|
102
|
+
record,
|
|
103
|
+
written,
|
|
104
|
+
original,
|
|
105
|
+
linked: linked && index > 0
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
#take(stack) {
|
|
109
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
110
|
+
if (hold) hold.lease = this;
|
|
111
|
+
return hold;
|
|
36
112
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
113
|
+
#acquire(element, stack) {
|
|
114
|
+
const current = element.getAttribute(this.#attribute);
|
|
115
|
+
const top = stack.at(-1);
|
|
116
|
+
const bottom = stack[0];
|
|
117
|
+
const over = top?.written === current ? top : void 0;
|
|
118
|
+
const from = this.#base ? bottom : over;
|
|
119
|
+
const hold = {
|
|
120
|
+
lease: this,
|
|
121
|
+
record: this.#record,
|
|
122
|
+
written: current,
|
|
123
|
+
original: from ? from.original : current,
|
|
124
|
+
linked: !this.#base && over !== void 0
|
|
125
|
+
};
|
|
126
|
+
if (this.#base && bottom) {
|
|
127
|
+
bottom.linked = true;
|
|
128
|
+
stack.unshift(hold);
|
|
129
|
+
} else stack.push(hold);
|
|
130
|
+
return hold;
|
|
131
|
+
}
|
|
132
|
+
#covered(element, stack, hold) {
|
|
133
|
+
const current = element.getAttribute(this.#attribute);
|
|
134
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
135
|
+
}
|
|
136
|
+
#mark(element, stack) {
|
|
137
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
138
|
+
stack.forEach((hold, index) => {
|
|
139
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
140
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
141
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
142
|
+
records.set(
|
|
143
|
+
hold.record,
|
|
144
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
for (const [name, recorded] of records) {
|
|
148
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
149
|
+
if (recorded === null) element.removeAttribute(name);
|
|
150
|
+
else element.setAttribute(name, recorded);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
#reflect(element, value) {
|
|
154
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
155
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
156
|
+
else element.setAttribute(this.#attribute, value);
|
|
42
157
|
}
|
|
43
158
|
};
|
|
44
159
|
|
|
45
|
-
|
|
160
|
+
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
161
|
+
"insertCompositionText",
|
|
162
|
+
"insertFromComposition",
|
|
163
|
+
"deleteCompositionText",
|
|
164
|
+
"deleteByComposition"
|
|
165
|
+
]);
|
|
46
166
|
var CompositionTracker = class {
|
|
47
167
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
48
168
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
49
169
|
#onStart;
|
|
50
170
|
#onEnd;
|
|
171
|
+
#confirmedTarget = null;
|
|
51
172
|
constructor(options = {}) {
|
|
52
173
|
this.#onStart = options.onStart;
|
|
53
174
|
this.#onEnd = options.onEnd;
|
|
54
175
|
}
|
|
55
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
56
176
|
observe(target) {
|
|
57
177
|
if (this.#observedTargets.has(target)) return;
|
|
58
178
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
59
179
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
180
|
+
target.addEventListener("keydown", this.#handleKeydown);
|
|
60
181
|
this.#observedTargets.add(target);
|
|
61
182
|
}
|
|
62
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
63
183
|
unobserve(target) {
|
|
64
184
|
if (!this.#observedTargets.delete(target)) return;
|
|
65
185
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
66
186
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
187
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
67
188
|
this.#activeTargets.delete(target);
|
|
189
|
+
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
68
190
|
}
|
|
69
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
70
191
|
disconnect() {
|
|
71
192
|
for (const target of this.#observedTargets) {
|
|
72
193
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
73
194
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
195
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
74
196
|
}
|
|
75
197
|
this.#observedTargets.clear();
|
|
76
198
|
this.#activeTargets.clear();
|
|
199
|
+
this.#confirmedTarget = null;
|
|
77
200
|
}
|
|
78
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
79
201
|
isComposing(event) {
|
|
80
202
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
81
203
|
}
|
|
204
|
+
consumesConfirmedInput(event) {
|
|
205
|
+
const confirmed = this.#confirmedTarget;
|
|
206
|
+
this.#confirmedTarget = null;
|
|
207
|
+
if (confirmed === null || confirmed !== event.target) return false;
|
|
208
|
+
const inputType = event.inputType;
|
|
209
|
+
return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
|
|
210
|
+
}
|
|
82
211
|
#handleStart = (event) => {
|
|
212
|
+
this.#confirmedTarget = null;
|
|
83
213
|
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
84
214
|
this.#onStart?.(event);
|
|
85
215
|
};
|
|
86
216
|
#handleEnd = (event) => {
|
|
87
217
|
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
218
|
+
this.#confirmedTarget = event.target;
|
|
88
219
|
this.#onEnd?.(event);
|
|
89
220
|
};
|
|
221
|
+
#handleKeydown = () => {
|
|
222
|
+
this.#confirmedTarget = null;
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
var DetachGate = class _DetachGate {
|
|
227
|
+
#pending = false;
|
|
228
|
+
get pending() {
|
|
229
|
+
return this.#pending;
|
|
230
|
+
}
|
|
231
|
+
static isDetached(host) {
|
|
232
|
+
return !host.element.isConnected || !_DetachGate.#listed(host);
|
|
233
|
+
}
|
|
234
|
+
disconnected(host, teardown) {
|
|
235
|
+
const carried = _DetachGate.#carried(host);
|
|
236
|
+
if (!carried && _DetachGate.isDetached(host)) {
|
|
237
|
+
this.#pending = false;
|
|
238
|
+
teardown();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this.#pending = true;
|
|
242
|
+
const probe = () => {
|
|
243
|
+
if (!this.#pending) return;
|
|
244
|
+
this.#pending = false;
|
|
245
|
+
teardown();
|
|
246
|
+
};
|
|
247
|
+
if (carried) setTimeout(probe, 0);
|
|
248
|
+
else queueMicrotask(probe);
|
|
249
|
+
}
|
|
250
|
+
static #carried(host) {
|
|
251
|
+
const element = host.element;
|
|
252
|
+
return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
|
|
253
|
+
}
|
|
254
|
+
static #listed(host) {
|
|
255
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
256
|
+
return tokens.includes(host.identifier);
|
|
257
|
+
}
|
|
258
|
+
cancel() {
|
|
259
|
+
this.#pending = false;
|
|
260
|
+
}
|
|
90
261
|
};
|
|
91
262
|
|
|
92
|
-
// src/utils/listener_set.ts
|
|
93
263
|
var ListenerSet = class {
|
|
94
|
-
/** The generation every `add` joins until the next `dispose()`. */
|
|
95
264
|
#abort = new AbortController();
|
|
96
|
-
/**
|
|
97
|
-
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
98
|
-
*
|
|
99
|
-
* The set supplies the signal, so `options` carries everything else the DOM
|
|
100
|
-
* accepts — `capture` included, which has to match at release time and no
|
|
101
|
-
* longer has a second place to drift from.
|
|
102
|
-
*/
|
|
103
265
|
add(target, type, handler, options) {
|
|
104
266
|
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
105
267
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Releases every listener of the open generation, synchronously, and opens the
|
|
108
|
-
* next one. Idempotent, and safe before anything has been added.
|
|
109
|
-
*/
|
|
110
268
|
dispose() {
|
|
111
269
|
this.#abort.abort();
|
|
112
270
|
this.#abort = new AbortController();
|
|
113
271
|
}
|
|
114
272
|
};
|
|
115
273
|
|
|
116
|
-
|
|
274
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
275
|
+
|
|
276
|
+
var NUMBER_BOUNDS = {
|
|
277
|
+
timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
|
|
278
|
+
function matchesNumberBounds(value, bounds) {
|
|
279
|
+
if (!Number.isFinite(value)) {
|
|
280
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
281
|
+
if (direction === null) return false;
|
|
282
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
283
|
+
}
|
|
284
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
285
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
286
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
287
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
288
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function readNumber(raw, fallback, bounds) {
|
|
293
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
var NumberValueReader = class {
|
|
297
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
298
|
+
read(owner, name, raw, fallback, bounds) {
|
|
299
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
300
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
301
|
+
this.#lastRejected.delete(name);
|
|
302
|
+
return resolved;
|
|
303
|
+
}
|
|
304
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
305
|
+
const literal = owner.element.getAttribute(attribute);
|
|
306
|
+
if (literal === null) {
|
|
307
|
+
this.#lastRejected.delete(name);
|
|
308
|
+
return resolved;
|
|
309
|
+
}
|
|
310
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
311
|
+
this.#lastRejected.set(name, literal);
|
|
312
|
+
console.warn(
|
|
313
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return resolved;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
117
320
|
var TimerRegistry = class {
|
|
118
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
119
321
|
ids = /* @__PURE__ */ new Set();
|
|
120
|
-
/**
|
|
121
|
-
* Cancels a single tracked timer.
|
|
122
|
-
*
|
|
123
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
124
|
-
* registry), so callers can clear defensively without guarding.
|
|
125
|
-
*/
|
|
126
322
|
clear(id) {
|
|
127
323
|
if (this.ids.delete(id)) {
|
|
128
324
|
this.cancel(id);
|
|
129
325
|
}
|
|
130
326
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
133
|
-
* to guarantee no timer outlives the element.
|
|
134
|
-
*/
|
|
135
327
|
clearAll() {
|
|
136
328
|
for (const id of this.ids) {
|
|
137
329
|
this.cancel(id);
|
|
138
330
|
}
|
|
139
331
|
this.ids.clear();
|
|
140
332
|
}
|
|
141
|
-
/** Number of timers currently tracked (pending). */
|
|
142
333
|
get size() {
|
|
143
334
|
return this.ids.size;
|
|
144
335
|
}
|
|
145
336
|
};
|
|
146
337
|
var SafeTimeout = class extends TimerRegistry {
|
|
147
|
-
/**
|
|
148
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
149
|
-
*
|
|
150
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
151
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
152
|
-
*/
|
|
153
338
|
set(callback, delay) {
|
|
154
339
|
const id = this.schedule(() => {
|
|
155
340
|
this.ids.delete(id);
|
|
@@ -166,8 +351,19 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
166
351
|
}
|
|
167
352
|
};
|
|
168
353
|
|
|
169
|
-
|
|
170
|
-
|
|
354
|
+
var TransientHooks = class {
|
|
355
|
+
#attributes;
|
|
356
|
+
constructor(options) {
|
|
357
|
+
this.#attributes = options.attributes;
|
|
358
|
+
}
|
|
359
|
+
reset(element) {
|
|
360
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
var PENDING = new TransientHooks({ attributes: ["data-auto-submit-pending"] });
|
|
365
|
+
var AutoSubmitController = class _AutoSubmitController extends Controller {
|
|
366
|
+
#numbers = new NumberValueReader();
|
|
171
367
|
static targets = ["form"];
|
|
172
368
|
static values = {
|
|
173
369
|
debounce: { type: Number, default: 300 },
|
|
@@ -175,61 +371,60 @@ var AutoSubmitController = class extends Controller {
|
|
|
175
371
|
announce: { type: Boolean, default: false },
|
|
176
372
|
message: { type: String, default: "" }
|
|
177
373
|
};
|
|
374
|
+
static valueConstraints = {
|
|
375
|
+
debounce: NUMBER_BOUNDS.timer
|
|
376
|
+
};
|
|
178
377
|
static actions = ["submit"];
|
|
179
378
|
static events = ["submit", "done", "reconcile"];
|
|
180
|
-
/** Debounce timer registry; one `clearAll()` in disconnect tears it down. */
|
|
181
379
|
#timers = new SafeTimeout();
|
|
182
|
-
/** Id of the pending debounce timer, so a new keystroke can reset it. */
|
|
183
380
|
#pendingId = 0;
|
|
184
|
-
/** Listeners that live with the bound form rather than with the connection. */
|
|
185
381
|
#formListeners = new ListenerSet();
|
|
186
|
-
/** The form the listeners are attached to; target callbacks rebind it. */
|
|
187
382
|
#boundForm = null;
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
/** Clears `aria-busy` and emits completion once Turbo finishes the submit. */
|
|
383
|
+
#busy = new AttributeLease("aria-busy", this.identifier);
|
|
384
|
+
#gate = new DetachGate();
|
|
191
385
|
#onSubmitEnd = () => {
|
|
192
|
-
this.#boundForm
|
|
386
|
+
if (this.#boundForm) this.#busy.return(this.#boundForm);
|
|
193
387
|
const message = this.messageValue;
|
|
194
388
|
this.dispatch("done", { detail: { message: message || void 0 } });
|
|
195
389
|
if (this.announceValue) announce(message);
|
|
196
390
|
};
|
|
197
|
-
/** Owns delegated IME lifecycle state and submits confirmed input text. */
|
|
198
391
|
#composition = new CompositionTracker({
|
|
199
392
|
onEnd: (event) => {
|
|
200
393
|
if (this.#triggers("input")) this.#schedule(event.target ?? null);
|
|
201
394
|
}
|
|
202
395
|
});
|
|
203
396
|
connect() {
|
|
204
|
-
this.#
|
|
205
|
-
this.#
|
|
397
|
+
const moved = this.#gate.pending;
|
|
398
|
+
this.#gate.cancel();
|
|
399
|
+
const form = this.#resolveForm();
|
|
400
|
+
if (moved && form && form === this.#boundForm) {
|
|
401
|
+
this.#composition.observe(form);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const stale = form?.hasAttribute("data-auto-submit-pending") === true;
|
|
405
|
+
if (form) PENDING.reset(form);
|
|
406
|
+
const busy = form !== null && this.#busy.return(form);
|
|
407
|
+
this.#bindForm(form);
|
|
408
|
+
if (stale || busy) this.dispatch("reconcile", { detail: {} });
|
|
206
409
|
}
|
|
207
410
|
disconnect() {
|
|
208
|
-
this.#beforeCache.deactivate();
|
|
209
|
-
this.#bindForm(null);
|
|
210
411
|
this.#composition.disconnect();
|
|
211
|
-
this.#
|
|
212
|
-
|
|
412
|
+
this.#gate.disconnected(this, () => {
|
|
413
|
+
this.#bindForm(null);
|
|
414
|
+
this.#pendingId = 0;
|
|
415
|
+
});
|
|
213
416
|
}
|
|
214
|
-
/** Follows a `form` target added (or swapped in) at runtime. */
|
|
215
417
|
formTargetConnected() {
|
|
216
418
|
this.#bindForm(this.#resolveForm());
|
|
217
419
|
}
|
|
218
|
-
/** Follows a `form` target removed (or swapped out) at runtime. */
|
|
219
420
|
formTargetDisconnected() {
|
|
220
421
|
this.#bindForm(this.#resolveForm());
|
|
221
422
|
}
|
|
222
|
-
/**
|
|
223
|
-
* Schedules a debounced submit. Wired to `input`/`change`; the `on` value is an
|
|
224
|
-
* allowlist so a configured subset (e.g. only `change`) is honored even when both
|
|
225
|
-
* are bound in markup. Coalesces rapid events into a single `requestSubmit`.
|
|
226
|
-
*/
|
|
227
423
|
submit(event) {
|
|
228
424
|
if (!this.#triggers(event.type)) return;
|
|
229
425
|
if (event.type === "input" && this.#composition.isComposing(event)) return;
|
|
230
426
|
this.#schedule(event.target ?? null);
|
|
231
427
|
}
|
|
232
|
-
/** Schedules (and coalesces) the debounced submit for the given trigger. */
|
|
233
428
|
#schedule(trigger) {
|
|
234
429
|
const form = this.#boundForm;
|
|
235
430
|
if (!form) return;
|
|
@@ -240,23 +435,15 @@ var AutoSubmitController = class extends Controller {
|
|
|
240
435
|
form.removeAttribute("data-auto-submit-pending");
|
|
241
436
|
this.dispatch("submit", { detail: { trigger } });
|
|
242
437
|
if (form.checkValidity()) {
|
|
243
|
-
|
|
438
|
+
this.#busy.write(form, "true");
|
|
244
439
|
}
|
|
245
440
|
form.requestSubmit();
|
|
246
|
-
}, this
|
|
441
|
+
}, this.#safeDebounce);
|
|
247
442
|
}
|
|
248
|
-
/** Cancels the pending debounced submit, if any (`clear` no-ops on unknown ids). */
|
|
249
443
|
#cancelPending() {
|
|
250
444
|
this.#timers.clear(this.#pendingId);
|
|
251
445
|
this.#pendingId = 0;
|
|
252
446
|
}
|
|
253
|
-
/**
|
|
254
|
-
* Replaces the subscribed form symmetrically. The outgoing form loses the
|
|
255
|
-
* `turbo:submit-end`/composition listeners, its pending hook, and any pending
|
|
256
|
-
* debounced submit (which described the outgoing form). An in-flight `aria-busy`
|
|
257
|
-
* is left for `turbo:submit-end` or the pre-cache rewind — removing it here
|
|
258
|
-
* would wipe a legitimately in-progress submission.
|
|
259
|
-
*/
|
|
260
447
|
#bindForm(form) {
|
|
261
448
|
if (form === this.#boundForm) return;
|
|
262
449
|
const previous = this.#boundForm;
|
|
@@ -271,25 +458,23 @@ var AutoSubmitController = class extends Controller {
|
|
|
271
458
|
this.#formListeners.add(form, "turbo:submit-end", this.#onSubmitEnd);
|
|
272
459
|
this.#composition.observe(form);
|
|
273
460
|
}
|
|
274
|
-
/** Resolves the form: the explicit `form` target, else a `<form>` root, else null. */
|
|
275
461
|
#resolveForm() {
|
|
276
462
|
if (this.hasFormTarget) return this.formTarget;
|
|
277
463
|
return this.element instanceof HTMLFormElement ? this.element : null;
|
|
278
464
|
}
|
|
279
|
-
/** Returns the transient state hooks to their initial (absent) state. */
|
|
280
|
-
#rewindForCache() {
|
|
281
|
-
const inProgress = this.#pendingId !== 0 || this.#boundForm?.hasAttribute("aria-busy") === true;
|
|
282
|
-
this.#cancelPending();
|
|
283
|
-
this.#boundForm?.removeAttribute("data-auto-submit-pending");
|
|
284
|
-
this.#boundForm?.removeAttribute("aria-busy");
|
|
285
|
-
if (inProgress) this.dispatch("reconcile", { detail: {} });
|
|
286
|
-
}
|
|
287
|
-
/** Whether `type` is one of the whitespace-separated event types in `on`. */
|
|
288
465
|
#triggers(type) {
|
|
289
466
|
return this.onValue.split(/\s+/).filter(Boolean).includes(type);
|
|
290
467
|
}
|
|
468
|
+
get #safeDebounce() {
|
|
469
|
+
return this.#numbers.read(
|
|
470
|
+
this,
|
|
471
|
+
"debounce",
|
|
472
|
+
this.debounceValue,
|
|
473
|
+
_AutoSubmitController.values.debounce.default,
|
|
474
|
+
_AutoSubmitController.valueConstraints.debounce
|
|
475
|
+
);
|
|
476
|
+
}
|
|
291
477
|
};
|
|
292
478
|
|
|
293
479
|
export { AutoSubmitController };
|
|
294
|
-
//# sourceMappingURL=auto_submit_controller.js.map
|
|
295
480
|
//# sourceMappingURL=auto_submit_controller.js.map
|