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/tags_input_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;
|
|
@@ -19,12 +16,10 @@ function fillTemplate(template, values) {
|
|
|
19
16
|
});
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
// src/utils/logical_scroll.ts
|
|
23
19
|
function isRtl(element) {
|
|
24
20
|
return window.getComputedStyle(element).direction === "rtl";
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
// src/utils/arrow_step.ts
|
|
28
23
|
function logicalArrowKey(key, element) {
|
|
29
24
|
if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
|
|
30
25
|
if (!isRtl(element)) return key;
|
|
@@ -35,7 +30,6 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
35
30
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
36
31
|
}
|
|
37
32
|
|
|
38
|
-
// src/utils/element_part.ts
|
|
39
33
|
function matchingPart(root, selector) {
|
|
40
34
|
return root.matches(selector) ? root : root.querySelector(selector);
|
|
41
35
|
}
|
|
@@ -54,31 +48,14 @@ function writeLabel(slot, text) {
|
|
|
54
48
|
else slot.prepend(text);
|
|
55
49
|
}
|
|
56
50
|
|
|
57
|
-
// src/utils/roving_tabindex.ts
|
|
58
51
|
var RovingTabindex = class {
|
|
59
|
-
/** Returns the current ordered item elements; called on every operation. */
|
|
60
52
|
#getItems;
|
|
61
|
-
/**
|
|
62
|
-
* @param getItems - Returns the current ordered item elements. Called on every
|
|
63
|
-
* operation so the live target list is always used.
|
|
64
|
-
*/
|
|
65
53
|
constructor(getItems) {
|
|
66
54
|
this.#getItems = getItems;
|
|
67
55
|
}
|
|
68
|
-
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
69
56
|
get activeIndex() {
|
|
70
57
|
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
71
58
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
74
|
-
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
75
|
-
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
76
|
-
* "nothing is currently tabbable".
|
|
77
|
-
*
|
|
78
|
-
* @param index - Position of the item to make tabbable.
|
|
79
|
-
* @param options - Pass `{ focus: true }` to also move DOM focus to that item,
|
|
80
|
-
* and `items` to reuse an event-scoped collection snapshot.
|
|
81
|
-
*/
|
|
82
59
|
setActive(index, options = {}) {
|
|
83
60
|
const { focus = false } = options;
|
|
84
61
|
const items = options.items ?? this.#getItems();
|
|
@@ -89,7 +66,6 @@ var RovingTabindex = class {
|
|
|
89
66
|
}
|
|
90
67
|
};
|
|
91
68
|
|
|
92
|
-
// src/utils/chip_row.ts
|
|
93
69
|
var ChipRow = class {
|
|
94
70
|
#directionElement;
|
|
95
71
|
#getItems;
|
|
@@ -105,10 +81,6 @@ var ChipRow = class {
|
|
|
105
81
|
this.#onRemove = options.onRemove;
|
|
106
82
|
this.#focusAfterEnd = options.focusAfterEnd;
|
|
107
83
|
}
|
|
108
|
-
/**
|
|
109
|
-
* Binds delegation to `container`; any row currently bound is released first.
|
|
110
|
-
* Idempotent for the same node, so target callbacks may call it freely.
|
|
111
|
-
*/
|
|
112
84
|
connect(container) {
|
|
113
85
|
if (this.#container === container) return;
|
|
114
86
|
this.disconnect();
|
|
@@ -116,10 +88,6 @@ var ChipRow = class {
|
|
|
116
88
|
container.addEventListener("click", this.#onClick);
|
|
117
89
|
container.addEventListener("keydown", this.#onKeydown);
|
|
118
90
|
}
|
|
119
|
-
/**
|
|
120
|
-
* Releases the current row. When `container` is supplied, a stale disconnect
|
|
121
|
-
* callback cannot tear listeners off a newer replacement target.
|
|
122
|
-
*/
|
|
123
91
|
disconnect(container) {
|
|
124
92
|
const current = this.#container;
|
|
125
93
|
if (!current || container !== void 0 && current !== container) return;
|
|
@@ -127,19 +95,15 @@ var ChipRow = class {
|
|
|
127
95
|
current.removeEventListener("keydown", this.#onKeydown);
|
|
128
96
|
this.#container = null;
|
|
129
97
|
}
|
|
130
|
-
/** Current remove buttons, at most one consumer-resolved button per chip item. */
|
|
131
98
|
get buttons() {
|
|
132
99
|
return this.#entries.map(({ button }) => button);
|
|
133
100
|
}
|
|
134
|
-
/** Number of currently navigable remove buttons. */
|
|
135
101
|
get length() {
|
|
136
102
|
return this.buttons.length;
|
|
137
103
|
}
|
|
138
|
-
/** Declared-item index of the last navigable chip, or `-1` for an empty row. */
|
|
139
104
|
get lastIndex() {
|
|
140
105
|
return this.#entries.at(-1)?.itemIndex ?? -1;
|
|
141
106
|
}
|
|
142
|
-
/** Keeps exactly one button tabbable, preferring the first authored Tab stop. */
|
|
143
107
|
ensureTabStop() {
|
|
144
108
|
const buttons = this.#entries.map(({ button }) => button);
|
|
145
109
|
const active = buttons.findIndex((button) => button.tabIndex === 0);
|
|
@@ -148,7 +112,6 @@ var ChipRow = class {
|
|
|
148
112
|
this.#roving.setActive(active === -1 ? 0 : active, { items: buttons });
|
|
149
113
|
}
|
|
150
114
|
}
|
|
151
|
-
/** Focuses the last chip, returning false for an empty row. */
|
|
152
115
|
focusLast() {
|
|
153
116
|
const buttons = this.#entries.map(({ button }) => button);
|
|
154
117
|
const last = buttons.length - 1;
|
|
@@ -156,10 +119,6 @@ var ChipRow = class {
|
|
|
156
119
|
this.#roving.setActive(last, { focus: true, items: buttons });
|
|
157
120
|
return true;
|
|
158
121
|
}
|
|
159
|
-
/**
|
|
160
|
-
* Focuses the chip that followed a removed index, or the new last chip.
|
|
161
|
-
* Returns false when removal emptied the row so the consumer can rescue focus.
|
|
162
|
-
*/
|
|
163
122
|
focusAfterRemoval(index) {
|
|
164
123
|
const entries = this.#entries;
|
|
165
124
|
if (entries.length === 0) return false;
|
|
@@ -170,14 +129,12 @@ var ChipRow = class {
|
|
|
170
129
|
});
|
|
171
130
|
return true;
|
|
172
131
|
}
|
|
173
|
-
/** Current remove-button entries, retaining each button's declared item index. */
|
|
174
132
|
get #entries() {
|
|
175
133
|
return this.#getItems().flatMap((item, itemIndex) => {
|
|
176
134
|
const button = this.#getButton(item);
|
|
177
135
|
return button ? [{ button, itemIndex }] : [];
|
|
178
136
|
});
|
|
179
137
|
}
|
|
180
|
-
/** Resolves a delegated event to the remove button owned by a declared chip. */
|
|
181
138
|
#entry(event) {
|
|
182
139
|
const target = event.target;
|
|
183
140
|
if (!(target instanceof Element)) return null;
|
|
@@ -188,13 +145,11 @@ var ChipRow = class {
|
|
|
188
145
|
const entry = entries[buttonIndex];
|
|
189
146
|
return { ...entry, buttonIndex, buttons: entries.map(({ button: button2 }) => button2) };
|
|
190
147
|
}
|
|
191
|
-
/** Delegates removal clicks without waiting for Stimulus to wire a new chip. */
|
|
192
148
|
#onClick = (event) => {
|
|
193
149
|
const entry = this.#entry(event);
|
|
194
150
|
if (!entry) return;
|
|
195
151
|
this.#onRemove(entry.itemIndex);
|
|
196
152
|
};
|
|
197
|
-
/** Applies the shared logical-arrow and removal policy within the chip row. */
|
|
198
153
|
#onKeydown = (event) => {
|
|
199
154
|
if (event.defaultPrevented || isReservedArrowChord(event)) return;
|
|
200
155
|
const entry = this.#entry(event);
|
|
@@ -225,7 +180,6 @@ var ChipRow = class {
|
|
|
225
180
|
};
|
|
226
181
|
};
|
|
227
182
|
|
|
228
|
-
// src/utils/composition_tracker.ts
|
|
229
183
|
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
230
184
|
"insertCompositionText",
|
|
231
185
|
"insertFromComposition",
|
|
@@ -237,13 +191,11 @@ var CompositionTracker = class {
|
|
|
237
191
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
238
192
|
#onStart;
|
|
239
193
|
#onEnd;
|
|
240
|
-
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
241
194
|
#confirmedTarget = null;
|
|
242
195
|
constructor(options = {}) {
|
|
243
196
|
this.#onStart = options.onStart;
|
|
244
197
|
this.#onEnd = options.onEnd;
|
|
245
198
|
}
|
|
246
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
247
199
|
observe(target) {
|
|
248
200
|
if (this.#observedTargets.has(target)) return;
|
|
249
201
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
@@ -251,7 +203,6 @@ var CompositionTracker = class {
|
|
|
251
203
|
target.addEventListener("keydown", this.#handleKeydown);
|
|
252
204
|
this.#observedTargets.add(target);
|
|
253
205
|
}
|
|
254
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
255
206
|
unobserve(target) {
|
|
256
207
|
if (!this.#observedTargets.delete(target)) return;
|
|
257
208
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
@@ -260,7 +211,6 @@ var CompositionTracker = class {
|
|
|
260
211
|
this.#activeTargets.delete(target);
|
|
261
212
|
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
262
213
|
}
|
|
263
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
264
214
|
disconnect() {
|
|
265
215
|
for (const target of this.#observedTargets) {
|
|
266
216
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
@@ -271,16 +221,9 @@ var CompositionTracker = class {
|
|
|
271
221
|
this.#activeTargets.clear();
|
|
272
222
|
this.#confirmedTarget = null;
|
|
273
223
|
}
|
|
274
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
275
224
|
isComposing(event) {
|
|
276
225
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
277
226
|
}
|
|
278
|
-
/**
|
|
279
|
-
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
280
|
-
*
|
|
281
|
-
* Asking closes the window either way, so one confirmation is folded at most
|
|
282
|
-
* once and a consumer asks once per `input`.
|
|
283
|
-
*/
|
|
284
227
|
consumesConfirmedInput(event) {
|
|
285
228
|
const confirmed = this.#confirmedTarget;
|
|
286
229
|
this.#confirmedTarget = null;
|
|
@@ -298,13 +241,11 @@ var CompositionTracker = class {
|
|
|
298
241
|
this.#confirmedTarget = event.target;
|
|
299
242
|
this.#onEnd?.(event);
|
|
300
243
|
};
|
|
301
|
-
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
302
244
|
#handleKeydown = () => {
|
|
303
245
|
this.#confirmedTarget = null;
|
|
304
246
|
};
|
|
305
247
|
};
|
|
306
248
|
|
|
307
|
-
// src/utils/field_mirror.ts
|
|
308
249
|
function writeFields(container, values, { name, form = "" }) {
|
|
309
250
|
const current = [...container.children];
|
|
310
251
|
const submits = current.length === values.length && current.every(
|
|
@@ -327,27 +268,33 @@ function commitField(target) {
|
|
|
327
268
|
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
328
269
|
}
|
|
329
270
|
|
|
330
|
-
|
|
271
|
+
var ListenerSet = class {
|
|
272
|
+
#abort = new AbortController();
|
|
273
|
+
add(target, type, handler, options) {
|
|
274
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
275
|
+
}
|
|
276
|
+
dispose() {
|
|
277
|
+
this.#abort.abort();
|
|
278
|
+
this.#abort = new AbortController();
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
331
282
|
var MicrotaskCoalescer = class {
|
|
332
283
|
#run;
|
|
333
284
|
#queued = false;
|
|
334
285
|
#active = false;
|
|
335
286
|
#generation = 0;
|
|
336
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
337
287
|
constructor(run) {
|
|
338
288
|
this.#run = run;
|
|
339
289
|
}
|
|
340
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
341
290
|
activate() {
|
|
342
291
|
this.#active = true;
|
|
343
292
|
}
|
|
344
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
345
293
|
cancel() {
|
|
346
294
|
this.#active = false;
|
|
347
295
|
this.#queued = false;
|
|
348
296
|
this.#generation += 1;
|
|
349
297
|
}
|
|
350
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
351
298
|
schedule() {
|
|
352
299
|
if (!this.#active || this.#queued) return;
|
|
353
300
|
this.#queued = true;
|
|
@@ -360,12 +307,90 @@ var MicrotaskCoalescer = class {
|
|
|
360
307
|
}
|
|
361
308
|
};
|
|
362
309
|
|
|
363
|
-
|
|
310
|
+
var MorphRenderWatcher = class {
|
|
311
|
+
#listeners = new ListenerSet();
|
|
312
|
+
#pass;
|
|
313
|
+
#element = null;
|
|
314
|
+
#onMorph = () => this.#pass.schedule();
|
|
315
|
+
constructor(run) {
|
|
316
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
317
|
+
}
|
|
318
|
+
observe(element) {
|
|
319
|
+
if (this.#element === element) return;
|
|
320
|
+
this.disconnect();
|
|
321
|
+
this.#element = element;
|
|
322
|
+
this.#pass.activate();
|
|
323
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
324
|
+
}
|
|
325
|
+
schedule() {
|
|
326
|
+
this.#pass.schedule();
|
|
327
|
+
}
|
|
328
|
+
disconnect() {
|
|
329
|
+
this.#listeners.dispose();
|
|
330
|
+
this.#pass.cancel();
|
|
331
|
+
this.#element = null;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
var MoveCounter = class {
|
|
336
|
+
#sequence = 0;
|
|
337
|
+
record() {
|
|
338
|
+
this.#sequence += 1;
|
|
339
|
+
return this.#sequence;
|
|
340
|
+
}
|
|
341
|
+
isLatest(token) {
|
|
342
|
+
return token === this.#sequence;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
var NUMBER_BOUNDS = {
|
|
347
|
+
nonNegative: { finite: true, min: 0 }};
|
|
348
|
+
function matchesNumberBounds(value, bounds) {
|
|
349
|
+
if (!Number.isFinite(value)) {
|
|
350
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
351
|
+
if (direction === null) return false;
|
|
352
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
353
|
+
}
|
|
354
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
355
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
356
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
357
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
358
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function readNumber(raw, fallback, bounds) {
|
|
363
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
var NumberValueReader = class {
|
|
367
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
368
|
+
read(owner, name, raw, fallback, bounds) {
|
|
369
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
370
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
371
|
+
this.#lastRejected.delete(name);
|
|
372
|
+
return resolved;
|
|
373
|
+
}
|
|
374
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
375
|
+
const literal = owner.element.getAttribute(attribute);
|
|
376
|
+
if (literal === null) {
|
|
377
|
+
this.#lastRejected.delete(name);
|
|
378
|
+
return resolved;
|
|
379
|
+
}
|
|
380
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
381
|
+
this.#lastRejected.set(name, literal);
|
|
382
|
+
console.warn(
|
|
383
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return resolved;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
364
390
|
function targetSelector(identifier, name) {
|
|
365
391
|
return `[data-${identifier}-target~="${name}"]`;
|
|
366
392
|
}
|
|
367
393
|
|
|
368
|
-
// src/utils/template_row.ts
|
|
369
394
|
function cloneTemplateRoot(template) {
|
|
370
395
|
const root = template.content.firstElementChild;
|
|
371
396
|
return root instanceof HTMLElement ? root.cloneNode(true) : null;
|
|
@@ -376,22 +401,15 @@ var TemplateRow = class {
|
|
|
376
401
|
constructor(options) {
|
|
377
402
|
this.#options = options;
|
|
378
403
|
}
|
|
379
|
-
/** The attribute selector for one declared part, in this controller's namespace. */
|
|
380
404
|
selector(name) {
|
|
381
405
|
return targetSelector(this.#options.identifier, name);
|
|
382
406
|
}
|
|
383
|
-
/** Re-arms the once-per-connection diagnostic. */
|
|
384
407
|
connect() {
|
|
385
408
|
this.#warned = false;
|
|
386
409
|
}
|
|
387
|
-
/**
|
|
388
|
-
* Names one missing part on the console, at most once per connection, and
|
|
389
|
-
* returns `null` so a caller can hand it straight back.
|
|
390
|
-
*/
|
|
391
410
|
report(missing) {
|
|
392
411
|
return this.#say(`lacks ${missing}`);
|
|
393
412
|
}
|
|
394
|
-
/** Writes one diagnostic per connection, saying what the template got wrong. */
|
|
395
413
|
#say(problem) {
|
|
396
414
|
if (this.#warned) return null;
|
|
397
415
|
this.#warned = true;
|
|
@@ -399,10 +417,6 @@ var TemplateRow = class {
|
|
|
399
417
|
console.warn(`Stimeo UI: "${identifier}" ${outcome} because its ${noun} ${problem}.`);
|
|
400
418
|
return null;
|
|
401
419
|
}
|
|
402
|
-
/**
|
|
403
|
-
* Clones the row and resolves its parts, or names what the template got wrong
|
|
404
|
-
* and returns `null`. `values` fills the button's authored `aria-label`.
|
|
405
|
-
*/
|
|
406
420
|
instantiate(template, values) {
|
|
407
421
|
const { root: rootName, required, optional, button: buttonName } = this.#options;
|
|
408
422
|
const root = cloneTemplateRoot(template);
|
|
@@ -430,7 +444,6 @@ var TemplateRow = class {
|
|
|
430
444
|
button.setAttribute("aria-label", fillTemplate(name, values));
|
|
431
445
|
return { root, slots, button };
|
|
432
446
|
}
|
|
433
|
-
/** The row may be its own part; otherwise the first descendant carrying the target. */
|
|
434
447
|
#resolve(root, name) {
|
|
435
448
|
return matchingPart(root, this.selector(name));
|
|
436
449
|
}
|
|
@@ -439,8 +452,9 @@ var TemplateRow = class {
|
|
|
439
452
|
}
|
|
440
453
|
};
|
|
441
454
|
|
|
442
|
-
|
|
443
|
-
|
|
455
|
+
var TagsInputController = class _TagsInputController extends Controller {
|
|
456
|
+
#numbers = new NumberValueReader();
|
|
457
|
+
#moves = new MoveCounter();
|
|
444
458
|
static targets = ["input", "tags", "tag", "tagTemplate", "label", "remove", "fields"];
|
|
445
459
|
static values = {
|
|
446
460
|
delimiter: { type: String, default: "," },
|
|
@@ -451,11 +465,12 @@ var TagsInputController = class extends Controller {
|
|
|
451
465
|
announceText: { type: String, default: "" },
|
|
452
466
|
announceRemovedText: { type: String, default: "" }
|
|
453
467
|
};
|
|
468
|
+
static valueConstraints = {
|
|
469
|
+
max: NUMBER_BOUNDS.nonNegative
|
|
470
|
+
};
|
|
454
471
|
static actions = ["onKeydown"];
|
|
455
472
|
static events = ["change", "reconcile", "reject"];
|
|
456
|
-
/** Last reconciled tag order, separating user edits from DOM/Turbo repair. */
|
|
457
473
|
#tagValues = [];
|
|
458
|
-
/** Builds one chip from the authored template and owns its diagnostic. */
|
|
459
474
|
#rows = new TemplateRow({
|
|
460
475
|
identifier: this.identifier,
|
|
461
476
|
root: "tag",
|
|
@@ -464,8 +479,7 @@ var TagsInputController = class extends Controller {
|
|
|
464
479
|
outcome: "added no tag",
|
|
465
480
|
noun: "chip template"
|
|
466
481
|
});
|
|
467
|
-
|
|
468
|
-
#reconcile = new MicrotaskCoalescer(() => this.#reconcileTags());
|
|
482
|
+
#reconcile = new MorphRenderWatcher(() => this.#reconcileTags());
|
|
469
483
|
#chipRow = new ChipRow({
|
|
470
484
|
directionElement: this.element,
|
|
471
485
|
getItems: () => this.tagTargets,
|
|
@@ -473,9 +487,7 @@ var TagsInputController = class extends Controller {
|
|
|
473
487
|
onRemove: (index) => this.#removeAt(index),
|
|
474
488
|
focusAfterEnd: () => this.#focusInput()
|
|
475
489
|
});
|
|
476
|
-
/** Owns IME lifecycle state for commit-key guarding. */
|
|
477
490
|
#composition = new CompositionTracker();
|
|
478
|
-
/** Wires tag-list keyboard navigation and removal, and seeds the single Tab stop. */
|
|
479
491
|
connect() {
|
|
480
492
|
this.#rows.connect();
|
|
481
493
|
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
@@ -483,56 +495,47 @@ var TagsInputController = class extends Controller {
|
|
|
483
495
|
const tags = this.#values;
|
|
484
496
|
this.#syncState(tags);
|
|
485
497
|
this.#tagValues = tags;
|
|
486
|
-
this.#reconcile.
|
|
498
|
+
this.#reconcile.observe(this.element);
|
|
487
499
|
}
|
|
488
|
-
/** Releases the delegated listeners so no handler outlives the element. */
|
|
489
500
|
disconnect() {
|
|
490
|
-
this.#reconcile.
|
|
501
|
+
this.#reconcile.disconnect();
|
|
491
502
|
this.#composition.disconnect();
|
|
492
503
|
this.#chipRow.disconnect();
|
|
493
504
|
}
|
|
494
|
-
/** Tracks an input added initially or after connect. */
|
|
495
505
|
inputTargetConnected(input) {
|
|
496
506
|
this.#composition.observe(input);
|
|
497
507
|
}
|
|
498
|
-
/** Removes composition listeners when the active input is replaced or removed. */
|
|
499
508
|
inputTargetDisconnected(input) {
|
|
500
509
|
this.#composition.unobserve(input);
|
|
501
510
|
}
|
|
502
|
-
/** Rebinds delegation and derived state when the tags container is replaced. */
|
|
503
511
|
tagsTargetConnected(_tags) {
|
|
504
512
|
this.#reconcile.schedule();
|
|
505
513
|
}
|
|
506
|
-
/** Releases only the row that disconnected, leaving a newer target intact. */
|
|
507
514
|
tagsTargetDisconnected(tags) {
|
|
508
515
|
this.#chipRow.disconnect(tags);
|
|
509
516
|
this.#reconcile.schedule();
|
|
510
517
|
}
|
|
511
|
-
/** Reconciles a tag inserted by Turbo, a server render, or another controller. */
|
|
512
518
|
tagTargetConnected(_tag) {
|
|
513
519
|
this.#reconcile.schedule();
|
|
514
520
|
}
|
|
515
|
-
/** Reconciles a tag removed by Turbo, a server render, or another controller. */
|
|
516
521
|
tagTargetDisconnected(_tag) {
|
|
517
522
|
this.#reconcile.schedule();
|
|
518
523
|
}
|
|
519
|
-
/** Seeds a fields target inserted after connect. */
|
|
520
524
|
fieldsTargetConnected() {
|
|
521
525
|
this.#reconcile.schedule();
|
|
522
526
|
}
|
|
523
|
-
|
|
527
|
+
fieldsTargetDisconnected() {
|
|
528
|
+
this.#reconcile.schedule();
|
|
529
|
+
}
|
|
524
530
|
nameValueChanged() {
|
|
525
531
|
this.#reconcile.schedule();
|
|
526
532
|
}
|
|
527
|
-
/** Repoints submitted fields when the owning form changes at runtime. */
|
|
528
533
|
formValueChanged() {
|
|
529
534
|
this.#reconcile.schedule();
|
|
530
535
|
}
|
|
531
|
-
/** Recomputes the full hook when the cap changes at runtime. */
|
|
532
536
|
maxValueChanged() {
|
|
533
537
|
this.#reconcile.schedule();
|
|
534
538
|
}
|
|
535
|
-
/** Commits on `Enter`/delimiter and deletes the last tag on empty `Backspace`. */
|
|
536
539
|
onKeydown(event) {
|
|
537
540
|
if (event.defaultPrevented) return;
|
|
538
541
|
if (isReservedArrowChord(event)) return;
|
|
@@ -558,18 +561,13 @@ var TagsInputController = class extends Controller {
|
|
|
558
561
|
}
|
|
559
562
|
}
|
|
560
563
|
}
|
|
561
|
-
/**
|
|
562
|
-
* Validates and adds the current input value as a tag, then clears the input.
|
|
563
|
-
*
|
|
564
|
-
* @stimeoRuntimeOnly `max` and `allowDuplicates` decide whether this one entry is taken.
|
|
565
|
-
*/
|
|
566
564
|
#commitInput() {
|
|
567
565
|
const value = this.inputTarget.value.trim();
|
|
568
566
|
if (value === "") {
|
|
569
567
|
this.#reject(value, "empty");
|
|
570
568
|
return;
|
|
571
569
|
}
|
|
572
|
-
if (this
|
|
570
|
+
if (this.#safeMax > 0 && this.tagTargets.length >= this.#safeMax) {
|
|
573
571
|
this.#reject(value, "max");
|
|
574
572
|
return;
|
|
575
573
|
}
|
|
@@ -580,18 +578,8 @@ var TagsInputController = class extends Controller {
|
|
|
580
578
|
if (!this.#appendTag(value)) return;
|
|
581
579
|
this.inputTarget.value = "";
|
|
582
580
|
const tags = this.#values;
|
|
583
|
-
this.#
|
|
584
|
-
this.#tagValues = tags;
|
|
585
|
-
this.#announceTransition(true, value, tags.length);
|
|
586
|
-
this.dispatch("change", { detail: { tags } });
|
|
581
|
+
this.#publishTags(tags, true, value);
|
|
587
582
|
}
|
|
588
|
-
/**
|
|
589
|
-
* Builds one chip from the template and appends it to the tag list.
|
|
590
|
-
*
|
|
591
|
-
* A refused row leaves the commit a no-op — nothing about the input, the tag
|
|
592
|
-
* set, the hidden fields, the announcement, or the events changes — and the
|
|
593
|
-
* row reports why on the console once per connection.
|
|
594
|
-
*/
|
|
595
583
|
#appendTag(value) {
|
|
596
584
|
if (!this.hasTagsTarget) return false;
|
|
597
585
|
if (!this.hasTagTemplateTarget) {
|
|
@@ -606,76 +594,79 @@ var TagsInputController = class extends Controller {
|
|
|
606
594
|
this.tagsTarget.appendChild(row.root);
|
|
607
595
|
return true;
|
|
608
596
|
}
|
|
609
|
-
/** Removes the tag at `index`, then applies the interaction-origin focus policy. */
|
|
610
597
|
#removeAt(index, focus = "neighbor") {
|
|
611
598
|
const tag = this.tagTargets[index];
|
|
612
599
|
if (!tag) return;
|
|
613
600
|
const value = tag.dataset.value ?? "";
|
|
614
601
|
tag.remove();
|
|
615
602
|
const tags = this.#values;
|
|
616
|
-
this.#
|
|
617
|
-
this.#tagValues = tags;
|
|
618
|
-
this.#announceTransition(false, value, tags.length);
|
|
619
|
-
this.dispatch("change", { detail: { tags } });
|
|
603
|
+
if (!this.#publishTags(tags, false, value)) return;
|
|
620
604
|
if (focus === "input") {
|
|
621
605
|
this.#focusInput();
|
|
622
606
|
return;
|
|
623
607
|
}
|
|
624
608
|
if (!this.#chipRow.focusAfterRemoval(index)) this.#focusInput();
|
|
625
609
|
}
|
|
626
|
-
/** Moves focus to the current input when one exists. */
|
|
627
610
|
#focusInput() {
|
|
628
611
|
if (this.hasInputTarget) this.inputTarget.focus();
|
|
629
612
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
*
|
|
633
|
-
* @stimeoRenderRoot
|
|
634
|
-
*/
|
|
635
|
-
#syncState(values, notify = false) {
|
|
613
|
+
#syncState(values) {
|
|
614
|
+
let fieldChanged = false;
|
|
636
615
|
if (this.hasFieldsTarget) {
|
|
637
616
|
const options = { name: this.nameValue, form: this.formValue };
|
|
638
|
-
|
|
639
|
-
commitField(this.fieldsTarget);
|
|
640
|
-
}
|
|
617
|
+
fieldChanged = writeFields(this.fieldsTarget, values, options);
|
|
641
618
|
}
|
|
642
|
-
const full = this
|
|
619
|
+
const full = this.#safeMax > 0 && values.length >= this.#safeMax;
|
|
643
620
|
this.element.toggleAttribute(`data-${this.identifier}-full`, full);
|
|
644
621
|
this.#chipRow.ensureTabStop();
|
|
622
|
+
return fieldChanged;
|
|
623
|
+
}
|
|
624
|
+
#publishTags(tags, added, value) {
|
|
625
|
+
const changed = !this.#sameTags(this.#tagValues, tags);
|
|
626
|
+
const fieldChanged = this.#syncState(tags);
|
|
627
|
+
this.#tagValues = tags;
|
|
628
|
+
if (!changed) return true;
|
|
629
|
+
const token = this.#moves.record();
|
|
630
|
+
if (fieldChanged) commitField(this.fieldsTarget);
|
|
631
|
+
if (!this.#moves.isLatest(token)) return false;
|
|
632
|
+
this.#announceTransition(added, value, tags.length);
|
|
633
|
+
if (!this.#moves.isLatest(token)) return false;
|
|
634
|
+
this.dispatch("change", { detail: { tags } });
|
|
635
|
+
return this.#moves.isLatest(token);
|
|
645
636
|
}
|
|
646
|
-
/** Repairs derived state after DOM/Turbo changes and reports a changed tag order. */
|
|
647
637
|
#reconcileTags() {
|
|
648
638
|
if (this.hasTagsTarget) this.#chipRow.connect(this.tagsTarget);
|
|
649
|
-
else this.#chipRow.disconnect();
|
|
650
639
|
const tags = this.#values;
|
|
651
640
|
this.#syncState(tags);
|
|
652
641
|
const changed = !this.#sameTags(this.#tagValues, tags);
|
|
653
642
|
this.#tagValues = tags;
|
|
654
|
-
if (changed)
|
|
643
|
+
if (changed) {
|
|
644
|
+
this.dispatch("reconcile", { detail: { tags } });
|
|
645
|
+
}
|
|
655
646
|
}
|
|
656
|
-
/**
|
|
657
|
-
* Sends one localized tag transition through the page's shared announcer.
|
|
658
|
-
*
|
|
659
|
-
* @stimeoRuntimeOnly The texts word the one announcement of this change.
|
|
660
|
-
*/
|
|
661
647
|
#announceTransition(added, value, count) {
|
|
662
648
|
const template = added ? this.announceTextValue : this.announceRemovedTextValue;
|
|
663
649
|
announce(fillTemplate(template, { label: value, value, count }));
|
|
664
650
|
}
|
|
665
|
-
/** Reports rejection with `{ value, reason: "duplicate" | "empty" | "max" }`. */
|
|
666
651
|
#reject(value, reason) {
|
|
667
652
|
this.dispatch("reject", { detail: { value, reason } });
|
|
668
653
|
}
|
|
669
|
-
/** Current tag values in order. */
|
|
670
654
|
get #values() {
|
|
671
655
|
return this.tagTargets.map((tag) => tag.dataset.value ?? "");
|
|
672
656
|
}
|
|
673
|
-
/** Whether two arrays carry the same values in the same submitted order. */
|
|
674
657
|
#sameTags(left, right) {
|
|
675
658
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
676
659
|
}
|
|
660
|
+
get #safeMax() {
|
|
661
|
+
return this.#numbers.read(
|
|
662
|
+
this,
|
|
663
|
+
"max",
|
|
664
|
+
this.maxValue,
|
|
665
|
+
_TagsInputController.values.max.default,
|
|
666
|
+
_TagsInputController.valueConstraints.max
|
|
667
|
+
);
|
|
668
|
+
}
|
|
677
669
|
};
|
|
678
670
|
|
|
679
671
|
export { TagsInputController };
|
|
680
|
-
//# sourceMappingURL=tags_input_controller.js.map
|
|
681
672
|
//# sourceMappingURL=tags_input_controller.js.map
|