stimeo-ui 0.4.0 → 0.6.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.
- package/CHANGELOG.md +184 -0
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.d.ts +3 -5
- package/dist/controllers/aspect_ratio_controller.js +19 -11
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.d.ts +36 -15
- package/dist/controllers/avatar_controller.js +195 -40
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +5 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +12 -3
- package/dist/controllers/carousel_controller.js +90 -10
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +31 -5
- package/dist/controllers/checkbox_controller.js +136 -25
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js +8 -3
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +4 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +9 -3
- package/dist/controllers/color_picker_controller.js +41 -11
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +1 -1
- package/dist/controllers/context_menu_controller.js +2 -2
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +5 -3
- package/dist/controllers/countdown_controller.js +5 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +23 -4
- package/dist/controllers/date_range_picker_controller.js +162 -31
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +1 -1
- package/dist/controllers/direct_upload_controller.js +3 -3
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +27 -7
- package/dist/controllers/empty_state_controller.js +107 -16
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +7 -1
- package/dist/controllers/file_dropzone_controller.js +26 -3
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +25 -5
- package/dist/controllers/flash_controller.js +161 -21
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +2 -3
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +8 -2
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +19 -2
- package/dist/controllers/frame_loading_controller.js +94 -22
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +8 -4
- package/dist/controllers/highlight_controller.js +38 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +2 -2
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +5 -4
- package/dist/controllers/idle_controller.js +39 -6
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +2 -0
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +1 -1
- package/dist/controllers/masonry_controller.js +1 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +5 -3
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +3 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +46 -12
- package/dist/controllers/multi_select_controller.js +460 -151
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +15 -13
- package/dist/controllers/network_status_controller.js +1 -3
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +29 -3
- package/dist/controllers/number_input_controller.js +455 -64
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +5 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js +38 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +4 -4
- package/dist/controllers/password_strength_controller.js +21 -3
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +4 -4
- package/dist/controllers/persist_controller.js +24 -5
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +10 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -2
- package/dist/controllers/portal_controller.js +10 -0
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +1 -1
- package/dist/controllers/progress_controller.js +7 -3
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +41 -15
- package/dist/controllers/radio_group_controller.js +540 -56
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +23 -0
- package/dist/controllers/range_slider_controller.js +385 -94
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +36 -31
- package/dist/controllers/rating_controller.js +274 -89
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +2 -0
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.js +33 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +4 -0
- package/dist/controllers/roving_controller.js +60 -5
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -1
- package/dist/controllers/scroll_area_controller.js +155 -23
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +33 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js +13 -17
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -2
- package/dist/controllers/skeleton_controller.js +71 -3
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +17 -1
- package/dist/controllers/slider_controller.js +325 -48
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +15 -10
- package/dist/controllers/spinner_controller.js +18 -3
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +1 -1
- package/dist/controllers/step_indicator_controller.js +3 -1
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.js +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -8
- package/dist/controllers/switch_controller.js +162 -18
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +41 -11
- package/dist/controllers/tags_input_controller.js +356 -120
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +42 -10
- package/dist/controllers/time_picker_controller.js +296 -104
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +39 -13
- package/dist/controllers/toggle_group_controller.js +378 -55
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +5 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +2 -2
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +2 -3
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +1 -2
- package/dist/controllers/tree_view_controller.js +24 -4
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +3811 -1048
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +92 -4
- package/dist/inspector/cli.js +174 -6
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +234 -8
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +24 -24
- package/dist/inspector/manifest.json +573 -59
- package/dist/positioning/index.d.ts +2 -2
- package/dist/positioning/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -2,6 +2,23 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/tags_input_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/announce.ts
|
|
6
|
+
function announce(message, options = {}) {
|
|
7
|
+
const text = message.trim();
|
|
8
|
+
if (text.length === 0) return;
|
|
9
|
+
window.dispatchEvent(
|
|
10
|
+
new CustomEvent("stimeo--announcer:announce", {
|
|
11
|
+
detail: { message: text, assertive: options.assertive === true }
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
function fillTemplate(template, values) {
|
|
16
|
+
return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
|
|
17
|
+
const replacement = values[name];
|
|
18
|
+
return replacement === void 0 ? match : String(replacement);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
// src/utils/logical_scroll.ts
|
|
6
23
|
function isRtl(element) {
|
|
7
24
|
return window.getComputedStyle(element).direction === "rtl";
|
|
@@ -18,6 +35,177 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
18
35
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
36
|
}
|
|
20
37
|
|
|
38
|
+
// src/utils/roving_tabindex.ts
|
|
39
|
+
var RovingTabindex = class {
|
|
40
|
+
/** Returns the current ordered item elements; called on every operation. */
|
|
41
|
+
#getItems;
|
|
42
|
+
/**
|
|
43
|
+
* @param getItems - Returns the current ordered item elements. Called on every
|
|
44
|
+
* operation so the live target list is always used.
|
|
45
|
+
*/
|
|
46
|
+
constructor(getItems) {
|
|
47
|
+
this.#getItems = getItems;
|
|
48
|
+
}
|
|
49
|
+
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
50
|
+
get activeIndex() {
|
|
51
|
+
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
55
|
+
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
56
|
+
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
57
|
+
* "nothing is currently tabbable".
|
|
58
|
+
*
|
|
59
|
+
* @param index - Position of the item to make tabbable.
|
|
60
|
+
* @param options - Pass `{ focus: true }` to also move DOM focus to that item,
|
|
61
|
+
* and `items` to reuse an event-scoped collection snapshot.
|
|
62
|
+
*/
|
|
63
|
+
setActive(index, options = {}) {
|
|
64
|
+
const { focus = false } = options;
|
|
65
|
+
const items = options.items ?? this.#getItems();
|
|
66
|
+
items.forEach((item, i) => {
|
|
67
|
+
item.tabIndex = i === index ? 0 : -1;
|
|
68
|
+
});
|
|
69
|
+
if (focus) items[index]?.focus();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/utils/chip_row.ts
|
|
74
|
+
var ChipRow = class {
|
|
75
|
+
#directionElement;
|
|
76
|
+
#getItems;
|
|
77
|
+
#getButton;
|
|
78
|
+
#onRemove;
|
|
79
|
+
#focusAfterEnd;
|
|
80
|
+
#roving = new RovingTabindex(() => this.buttons);
|
|
81
|
+
#container = null;
|
|
82
|
+
constructor(options) {
|
|
83
|
+
this.#directionElement = options.directionElement;
|
|
84
|
+
this.#getItems = options.getItems;
|
|
85
|
+
this.#getButton = options.getButton ?? ((item) => item.querySelector("button"));
|
|
86
|
+
this.#onRemove = options.onRemove;
|
|
87
|
+
this.#focusAfterEnd = options.focusAfterEnd;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Binds delegation to `container`; any row currently bound is released first.
|
|
91
|
+
* Idempotent for the same node, so target callbacks may call it freely.
|
|
92
|
+
*/
|
|
93
|
+
connect(container) {
|
|
94
|
+
if (this.#container === container) return;
|
|
95
|
+
this.disconnect();
|
|
96
|
+
this.#container = container;
|
|
97
|
+
container.addEventListener("click", this.#onClick);
|
|
98
|
+
container.addEventListener("keydown", this.#onKeydown);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Releases the current row. When `container` is supplied, a stale disconnect
|
|
102
|
+
* callback cannot tear listeners off a newer replacement target.
|
|
103
|
+
*/
|
|
104
|
+
disconnect(container) {
|
|
105
|
+
const current = this.#container;
|
|
106
|
+
if (!current || container !== void 0 && current !== container) return;
|
|
107
|
+
current.removeEventListener("click", this.#onClick);
|
|
108
|
+
current.removeEventListener("keydown", this.#onKeydown);
|
|
109
|
+
this.#container = null;
|
|
110
|
+
}
|
|
111
|
+
/** Current remove buttons, at most one consumer-resolved button per chip item. */
|
|
112
|
+
get buttons() {
|
|
113
|
+
return this.#entries.map(({ button }) => button);
|
|
114
|
+
}
|
|
115
|
+
/** Number of currently navigable remove buttons. */
|
|
116
|
+
get length() {
|
|
117
|
+
return this.buttons.length;
|
|
118
|
+
}
|
|
119
|
+
/** Declared-item index of the last navigable chip, or `-1` for an empty row. */
|
|
120
|
+
get lastIndex() {
|
|
121
|
+
return this.#entries.at(-1)?.itemIndex ?? -1;
|
|
122
|
+
}
|
|
123
|
+
/** Keeps exactly one button tabbable, preferring the first authored Tab stop. */
|
|
124
|
+
ensureTabStop() {
|
|
125
|
+
const buttons = this.#entries.map(({ button }) => button);
|
|
126
|
+
const active = buttons.findIndex((button) => button.tabIndex === 0);
|
|
127
|
+
const count = buttons.filter((button) => button.tabIndex === 0).length;
|
|
128
|
+
if (buttons.length > 0 && count !== 1) {
|
|
129
|
+
this.#roving.setActive(active === -1 ? 0 : active, { items: buttons });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Focuses the last chip, returning false for an empty row. */
|
|
133
|
+
focusLast() {
|
|
134
|
+
const buttons = this.#entries.map(({ button }) => button);
|
|
135
|
+
const last = buttons.length - 1;
|
|
136
|
+
if (last < 0) return false;
|
|
137
|
+
this.#roving.setActive(last, { focus: true, items: buttons });
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Focuses the chip that followed a removed index, or the new last chip.
|
|
142
|
+
* Returns false when removal emptied the row so the consumer can rescue focus.
|
|
143
|
+
*/
|
|
144
|
+
focusAfterRemoval(index) {
|
|
145
|
+
const entries = this.#entries;
|
|
146
|
+
if (entries.length === 0) return false;
|
|
147
|
+
const following = entries.findIndex((entry) => entry.itemIndex >= index);
|
|
148
|
+
this.#roving.setActive(following === -1 ? entries.length - 1 : following, {
|
|
149
|
+
focus: true,
|
|
150
|
+
items: entries.map(({ button }) => button)
|
|
151
|
+
});
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
/** Current remove-button entries, retaining each button's declared item index. */
|
|
155
|
+
get #entries() {
|
|
156
|
+
return this.#getItems().flatMap((item, itemIndex) => {
|
|
157
|
+
const button = this.#getButton(item);
|
|
158
|
+
return button ? [{ button, itemIndex }] : [];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/** Resolves a delegated event to the remove button owned by a declared chip. */
|
|
162
|
+
#entry(event) {
|
|
163
|
+
const target = event.target;
|
|
164
|
+
if (!(target instanceof Element)) return null;
|
|
165
|
+
const button = target.closest("button");
|
|
166
|
+
const entries = this.#entries;
|
|
167
|
+
const buttonIndex = entries.findIndex((entry2) => entry2.button === button);
|
|
168
|
+
if (buttonIndex === -1) return null;
|
|
169
|
+
const entry = entries[buttonIndex];
|
|
170
|
+
return { ...entry, buttonIndex, buttons: entries.map(({ button: button2 }) => button2) };
|
|
171
|
+
}
|
|
172
|
+
/** Delegates removal clicks without waiting for Stimulus to wire a new chip. */
|
|
173
|
+
#onClick = (event) => {
|
|
174
|
+
const entry = this.#entry(event);
|
|
175
|
+
if (!entry) return;
|
|
176
|
+
this.#onRemove(entry.itemIndex);
|
|
177
|
+
};
|
|
178
|
+
/** Applies the shared logical-arrow and removal policy within the chip row. */
|
|
179
|
+
#onKeydown = (event) => {
|
|
180
|
+
if (event.defaultPrevented || isReservedArrowChord(event)) return;
|
|
181
|
+
const entry = this.#entry(event);
|
|
182
|
+
if (!entry) return;
|
|
183
|
+
const buttons = entry.buttons;
|
|
184
|
+
const index = entry.buttonIndex;
|
|
185
|
+
switch (logicalArrowKey(event.key, this.#directionElement)) {
|
|
186
|
+
case "ArrowLeft":
|
|
187
|
+
if (index > 0) {
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
this.#roving.setActive(index - 1, { focus: true, items: buttons });
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
case "ArrowRight":
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
if (index < buttons.length - 1) {
|
|
195
|
+
this.#roving.setActive(index + 1, { focus: true, items: buttons });
|
|
196
|
+
} else {
|
|
197
|
+
this.#focusAfterEnd();
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
case "Delete":
|
|
201
|
+
case "Backspace":
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
this.#onRemove(entry.itemIndex);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
21
209
|
// src/utils/composition_tracker.ts
|
|
22
210
|
var CompositionTracker = class {
|
|
23
211
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
@@ -65,65 +253,82 @@ var CompositionTracker = class {
|
|
|
65
253
|
};
|
|
66
254
|
};
|
|
67
255
|
|
|
68
|
-
// src/utils/
|
|
69
|
-
var
|
|
70
|
-
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.#getItems = getItems;
|
|
256
|
+
// src/utils/microtask_coalescer.ts
|
|
257
|
+
var MicrotaskCoalescer = class {
|
|
258
|
+
#run;
|
|
259
|
+
#queued = false;
|
|
260
|
+
#active = false;
|
|
261
|
+
#generation = 0;
|
|
262
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
263
|
+
constructor(run) {
|
|
264
|
+
this.#run = run;
|
|
78
265
|
}
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
266
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
267
|
+
activate() {
|
|
268
|
+
this.#active = true;
|
|
82
269
|
}
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
270
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
271
|
+
cancel() {
|
|
272
|
+
this.#active = false;
|
|
273
|
+
this.#queued = false;
|
|
274
|
+
this.#generation += 1;
|
|
275
|
+
}
|
|
276
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
277
|
+
schedule() {
|
|
278
|
+
if (!this.#active || this.#queued) return;
|
|
279
|
+
this.#queued = true;
|
|
280
|
+
const generation = this.#generation;
|
|
281
|
+
queueMicrotask(() => {
|
|
282
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
283
|
+
this.#queued = false;
|
|
284
|
+
this.#run();
|
|
96
285
|
});
|
|
97
|
-
if (focus) items[index]?.focus();
|
|
98
286
|
}
|
|
99
287
|
};
|
|
100
288
|
|
|
101
289
|
// src/controllers/tags_input_controller.ts
|
|
102
290
|
var TagsInputController = class extends Controller {
|
|
103
|
-
static targets = ["input", "tags", "tag", "tagTemplate", "
|
|
291
|
+
static targets = ["input", "tags", "tag", "tagTemplate", "label", "remove", "fields"];
|
|
104
292
|
static values = {
|
|
105
293
|
delimiter: { type: String, default: "," },
|
|
106
294
|
max: { type: Number, default: 0 },
|
|
107
295
|
allowDuplicates: { type: Boolean, default: false },
|
|
108
|
-
name: { type: String, default: "tags[]" }
|
|
296
|
+
name: { type: String, default: "tags[]" },
|
|
297
|
+
announceText: { type: String, default: "" },
|
|
298
|
+
announceRemovedText: { type: String, default: "" }
|
|
109
299
|
};
|
|
110
300
|
static actions = ["onKeydown"];
|
|
111
|
-
static events = ["change", "reject"];
|
|
112
|
-
|
|
301
|
+
static events = ["change", "reconcile", "reject"];
|
|
302
|
+
/** Last reconciled tag order, separating user edits from DOM/Turbo repair. */
|
|
303
|
+
#tagValues = [];
|
|
304
|
+
/** Whether this connection already reported its unusable chip template. */
|
|
305
|
+
#warnedTemplate = false;
|
|
306
|
+
/** Collapses one target/Value mutation batch into one final-DOM repair pass. */
|
|
307
|
+
#reconcile = new MicrotaskCoalescer(() => this.#reconcileTags());
|
|
308
|
+
#chipRow = new ChipRow({
|
|
309
|
+
directionElement: this.element,
|
|
310
|
+
getItems: () => this.tagTargets,
|
|
311
|
+
getButton: (tag) => tag.querySelector('button[data-stimeo--tags-input-target~="remove"]'),
|
|
312
|
+
onRemove: (index) => this.#removeAt(index),
|
|
313
|
+
focusAfterEnd: () => this.#focusInput()
|
|
314
|
+
});
|
|
113
315
|
/** Owns IME lifecycle state for commit-key guarding. */
|
|
114
316
|
#composition = new CompositionTracker();
|
|
115
317
|
/** Wires tag-list keyboard navigation and removal, and seeds the single Tab stop. */
|
|
116
318
|
connect() {
|
|
319
|
+
this.#warnedTemplate = false;
|
|
117
320
|
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
118
|
-
this.
|
|
119
|
-
|
|
120
|
-
this.#syncState();
|
|
321
|
+
if (this.hasTagsTarget) this.#chipRow.connect(this.tagsTarget);
|
|
322
|
+
const tags = this.#values;
|
|
323
|
+
this.#syncState(tags);
|
|
324
|
+
this.#tagValues = tags;
|
|
325
|
+
this.#reconcile.activate();
|
|
121
326
|
}
|
|
122
327
|
/** Releases the delegated listeners so no handler outlives the element. */
|
|
123
328
|
disconnect() {
|
|
329
|
+
this.#reconcile.cancel();
|
|
124
330
|
this.#composition.disconnect();
|
|
125
|
-
this.
|
|
126
|
-
this.tagsTarget.removeEventListener("click", this.#onTagClick);
|
|
331
|
+
this.#chipRow.disconnect();
|
|
127
332
|
}
|
|
128
333
|
/** Tracks an input added initially or after connect. */
|
|
129
334
|
inputTargetConnected(input) {
|
|
@@ -133,10 +338,40 @@ var TagsInputController = class extends Controller {
|
|
|
133
338
|
inputTargetDisconnected(input) {
|
|
134
339
|
this.#composition.unobserve(input);
|
|
135
340
|
}
|
|
341
|
+
/** Rebinds delegation and derived state when the tags container is replaced. */
|
|
342
|
+
tagsTargetConnected(_tags) {
|
|
343
|
+
this.#reconcile.schedule();
|
|
344
|
+
}
|
|
345
|
+
/** Releases only the row that disconnected, leaving a newer target intact. */
|
|
346
|
+
tagsTargetDisconnected(tags) {
|
|
347
|
+
this.#chipRow.disconnect(tags);
|
|
348
|
+
this.#reconcile.schedule();
|
|
349
|
+
}
|
|
350
|
+
/** Reconciles a tag inserted by Turbo, a server render, or another controller. */
|
|
351
|
+
tagTargetConnected(_tag) {
|
|
352
|
+
this.#reconcile.schedule();
|
|
353
|
+
}
|
|
354
|
+
/** Reconciles a tag removed by Turbo, a server render, or another controller. */
|
|
355
|
+
tagTargetDisconnected(_tag) {
|
|
356
|
+
this.#reconcile.schedule();
|
|
357
|
+
}
|
|
358
|
+
/** Seeds a fields target inserted after connect. */
|
|
359
|
+
fieldsTargetConnected() {
|
|
360
|
+
this.#reconcile.schedule();
|
|
361
|
+
}
|
|
362
|
+
/** Rebuilds submitted fields when the public name changes at runtime. */
|
|
363
|
+
nameValueChanged() {
|
|
364
|
+
this.#reconcile.schedule();
|
|
365
|
+
}
|
|
366
|
+
/** Recomputes the full hook when the cap changes at runtime. */
|
|
367
|
+
maxValueChanged() {
|
|
368
|
+
this.#reconcile.schedule();
|
|
369
|
+
}
|
|
136
370
|
/** Commits on `Enter`/delimiter and deletes the last tag on empty `Backspace`. */
|
|
137
371
|
onKeydown(event) {
|
|
138
372
|
if (event.defaultPrevented) return;
|
|
139
373
|
if (isReservedArrowChord(event)) return;
|
|
374
|
+
if (!this.hasInputTarget) return;
|
|
140
375
|
if (this.#composition.isComposing(event)) return;
|
|
141
376
|
if (event.key === "Enter" || event.key === this.delimiterValue) {
|
|
142
377
|
event.preventDefault();
|
|
@@ -144,33 +379,20 @@ var TagsInputController = class extends Controller {
|
|
|
144
379
|
return;
|
|
145
380
|
}
|
|
146
381
|
if (event.key === "Backspace" && this.inputTarget.value === "") {
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
382
|
+
const last = this.#chipRow.lastIndex;
|
|
383
|
+
if (last >= 0) {
|
|
149
384
|
event.preventDefault();
|
|
150
|
-
this.#removeAt(
|
|
385
|
+
this.#removeAt(last, "input");
|
|
151
386
|
}
|
|
152
387
|
return;
|
|
153
388
|
}
|
|
154
389
|
if (logicalArrowKey(event.key, this.element) === "ArrowLeft" && this.inputTarget.value === "") {
|
|
155
|
-
|
|
156
|
-
if (buttons.length > 0) {
|
|
390
|
+
if (this.#chipRow.length > 0) {
|
|
157
391
|
event.preventDefault();
|
|
158
|
-
this.#
|
|
392
|
+
this.#chipRow.focusLast();
|
|
159
393
|
}
|
|
160
394
|
}
|
|
161
395
|
}
|
|
162
|
-
/**
|
|
163
|
-
* Deletes the tag whose remove button was clicked. Delegated on the tags
|
|
164
|
-
* container (like `#onTagKeydown`) rather than bound per chip via
|
|
165
|
-
* `data-action`, so it works the instant a chip is appended without waiting on
|
|
166
|
-
* Stimulus to wire a freshly created element.
|
|
167
|
-
*/
|
|
168
|
-
#onTagClick = (event) => {
|
|
169
|
-
const button = event.target.closest("button");
|
|
170
|
-
if (!button || !this.tagsTarget.contains(button)) return;
|
|
171
|
-
const index = this.#removeButtons.indexOf(button);
|
|
172
|
-
if (index !== -1) this.#removeAt(index);
|
|
173
|
-
};
|
|
174
396
|
/** Validates and adds the current input value as a tag, then clears the input. */
|
|
175
397
|
#commitInput() {
|
|
176
398
|
const value = this.inputTarget.value.trim();
|
|
@@ -186,78 +408,83 @@ var TagsInputController = class extends Controller {
|
|
|
186
408
|
this.#reject(value, "duplicate");
|
|
187
409
|
return;
|
|
188
410
|
}
|
|
189
|
-
this.#appendTag(value);
|
|
411
|
+
if (!this.#appendTag(value)) return;
|
|
190
412
|
this.inputTarget.value = "";
|
|
191
|
-
this.#
|
|
192
|
-
this.#syncState();
|
|
193
|
-
this
|
|
413
|
+
const tags = this.#values;
|
|
414
|
+
this.#syncState(tags);
|
|
415
|
+
this.#tagValues = tags;
|
|
416
|
+
this.#announceTransition(true, value, tags.length);
|
|
417
|
+
this.dispatch("change", { detail: { tags } });
|
|
194
418
|
}
|
|
195
419
|
/** Builds one chip from the template and appends it to the tag list. */
|
|
196
420
|
#appendTag(value) {
|
|
197
|
-
if (!this.
|
|
421
|
+
if (!this.hasTagsTarget) return false;
|
|
422
|
+
if (!this.hasTagTemplateTarget) return this.#warnTemplate('a "tagTemplate" target');
|
|
198
423
|
const fragment = this.tagTemplateTarget.content.cloneNode(true);
|
|
199
|
-
const tag = fragment.querySelector('[data-stimeo--tags-input-target
|
|
200
|
-
const label = fragment.querySelector('[data-tags-input-
|
|
201
|
-
const button = fragment.querySelector(
|
|
202
|
-
|
|
424
|
+
const tag = fragment.querySelector('[data-stimeo--tags-input-target~="tag"]');
|
|
425
|
+
const label = fragment.querySelector('[data-stimeo--tags-input-target~="label"]');
|
|
426
|
+
const button = fragment.querySelector(
|
|
427
|
+
'button[data-stimeo--tags-input-target~="remove"]'
|
|
428
|
+
);
|
|
429
|
+
const removeName = button?.getAttribute("aria-label")?.trim() ?? "";
|
|
430
|
+
if (!tag) return this.#warnTemplate('a "tag" target');
|
|
431
|
+
if (!label) return this.#warnTemplate('a "label" target');
|
|
432
|
+
if (!button) return this.#warnTemplate('a "remove" target <button>');
|
|
433
|
+
if (removeName === "") {
|
|
434
|
+
return this.#warnTemplate('a non-empty aria-label on its "remove" target');
|
|
435
|
+
}
|
|
203
436
|
tag.dataset.value = value;
|
|
204
|
-
|
|
205
|
-
button.setAttribute("aria-label",
|
|
437
|
+
label.textContent = value;
|
|
438
|
+
button.setAttribute("aria-label", fillTemplate(removeName, { label: value, value }));
|
|
206
439
|
button.tabIndex = -1;
|
|
207
440
|
this.tagsTarget.appendChild(fragment);
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Reports an unusable chip template to the author, once per connection.
|
|
445
|
+
*
|
|
446
|
+
* The commit itself stays a no-op — nothing about the input, the tag set, the
|
|
447
|
+
* hidden fields, the announcement, or the events changes. Without this line
|
|
448
|
+
* the only symptom is a field that accepts no tags at all, and the two causes
|
|
449
|
+
* the Inspector cannot see statically (a name that renders empty from a
|
|
450
|
+
* missing translation, a server-rendered template) would have no diagnostic
|
|
451
|
+
* anywhere.
|
|
452
|
+
*/
|
|
453
|
+
#warnTemplate(missing) {
|
|
454
|
+
if (!this.#warnedTemplate) {
|
|
455
|
+
this.#warnedTemplate = true;
|
|
456
|
+
console.warn(
|
|
457
|
+
`Stimeo UI: "${this.identifier}" added no tag because its chip template lacks ${missing}.`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
208
461
|
}
|
|
209
|
-
/** Removes the tag at `index`, then
|
|
210
|
-
#removeAt(index) {
|
|
462
|
+
/** Removes the tag at `index`, then applies the interaction-origin focus policy. */
|
|
463
|
+
#removeAt(index, focus = "neighbor") {
|
|
211
464
|
const tag = this.tagTargets[index];
|
|
212
465
|
if (!tag) return;
|
|
213
466
|
const value = tag.dataset.value ?? "";
|
|
214
467
|
tag.remove();
|
|
215
|
-
this.#
|
|
216
|
-
this.#syncState();
|
|
217
|
-
this
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
468
|
+
const tags = this.#values;
|
|
469
|
+
this.#syncState(tags);
|
|
470
|
+
this.#tagValues = tags;
|
|
471
|
+
this.#announceTransition(false, value, tags.length);
|
|
472
|
+
this.dispatch("change", { detail: { tags } });
|
|
473
|
+
if (focus === "input") {
|
|
474
|
+
this.#focusInput();
|
|
475
|
+
return;
|
|
223
476
|
}
|
|
477
|
+
if (!this.#chipRow.focusAfterRemoval(index)) this.#focusInput();
|
|
478
|
+
}
|
|
479
|
+
/** Moves focus to the current input when one exists. */
|
|
480
|
+
#focusInput() {
|
|
481
|
+
if (this.hasInputTarget) this.inputTarget.focus();
|
|
224
482
|
}
|
|
225
|
-
/** Handles arrow navigation and deletion within the chip list (delegated). */
|
|
226
|
-
#onTagKeydown = (event) => {
|
|
227
|
-
if (event.defaultPrevented) return;
|
|
228
|
-
if (isReservedArrowChord(event)) return;
|
|
229
|
-
const button = event.target.closest("button");
|
|
230
|
-
if (!button) return;
|
|
231
|
-
const buttons = this.#removeButtons;
|
|
232
|
-
const index = buttons.indexOf(button);
|
|
233
|
-
if (index === -1) return;
|
|
234
|
-
switch (logicalArrowKey(event.key, this.element)) {
|
|
235
|
-
case "ArrowLeft":
|
|
236
|
-
if (index > 0) {
|
|
237
|
-
event.preventDefault();
|
|
238
|
-
this.#roving.setActive(index - 1, { focus: true });
|
|
239
|
-
}
|
|
240
|
-
break;
|
|
241
|
-
case "ArrowRight":
|
|
242
|
-
event.preventDefault();
|
|
243
|
-
if (index < buttons.length - 1) {
|
|
244
|
-
this.#roving.setActive(index + 1, { focus: true });
|
|
245
|
-
} else {
|
|
246
|
-
this.inputTarget.focus();
|
|
247
|
-
}
|
|
248
|
-
break;
|
|
249
|
-
case "Delete":
|
|
250
|
-
case "Backspace":
|
|
251
|
-
event.preventDefault();
|
|
252
|
-
this.#removeAt(index);
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
483
|
/** Rebuilds the hidden form fields, the `full` flag, and the roving Tab stop. */
|
|
257
|
-
#syncState() {
|
|
484
|
+
#syncState(values) {
|
|
258
485
|
if (this.hasFieldsTarget) {
|
|
259
486
|
this.fieldsTarget.replaceChildren(
|
|
260
|
-
...
|
|
487
|
+
...values.map((value) => {
|
|
261
488
|
const input = document.createElement("input");
|
|
262
489
|
input.type = "hidden";
|
|
263
490
|
input.name = this.nameValue;
|
|
@@ -266,28 +493,37 @@ var TagsInputController = class extends Controller {
|
|
|
266
493
|
})
|
|
267
494
|
);
|
|
268
495
|
}
|
|
269
|
-
const full = this.maxValue > 0 &&
|
|
496
|
+
const full = this.maxValue > 0 && values.length >= this.maxValue;
|
|
270
497
|
this.element.toggleAttribute("data-stimeo--tags-input-full", full);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
498
|
+
this.#chipRow.ensureTabStop();
|
|
499
|
+
}
|
|
500
|
+
/** Repairs derived state after DOM/Turbo changes and reports a changed tag order. */
|
|
501
|
+
#reconcileTags() {
|
|
502
|
+
if (this.hasTagsTarget) this.#chipRow.connect(this.tagsTarget);
|
|
503
|
+
else this.#chipRow.disconnect();
|
|
504
|
+
const tags = this.#values;
|
|
505
|
+
this.#syncState(tags);
|
|
506
|
+
const changed = !this.#sameTags(this.#tagValues, tags);
|
|
507
|
+
this.#tagValues = tags;
|
|
508
|
+
if (changed) this.dispatch("reconcile", { detail: { tags } });
|
|
274
509
|
}
|
|
275
|
-
/**
|
|
276
|
-
#
|
|
277
|
-
|
|
510
|
+
/** Sends one localized tag transition through the page's shared announcer. */
|
|
511
|
+
#announceTransition(added, value, count) {
|
|
512
|
+
const template = added ? this.announceTextValue : this.announceRemovedTextValue;
|
|
513
|
+
announce(fillTemplate(template, { label: value, value, count }));
|
|
278
514
|
}
|
|
279
|
-
/** Reports
|
|
515
|
+
/** Reports rejection with `{ value, reason: "duplicate" | "empty" | "max" }`. */
|
|
280
516
|
#reject(value, reason) {
|
|
281
517
|
this.dispatch("reject", { detail: { value, reason } });
|
|
282
518
|
}
|
|
283
|
-
/** The remove buttons in document order (the roving navigation set). */
|
|
284
|
-
get #removeButtons() {
|
|
285
|
-
return Array.from(this.tagsTarget.querySelectorAll("button"));
|
|
286
|
-
}
|
|
287
519
|
/** Current tag values in order. */
|
|
288
520
|
get #values() {
|
|
289
521
|
return this.tagTargets.map((tag) => tag.dataset.value ?? "");
|
|
290
522
|
}
|
|
523
|
+
/** Whether two arrays carry the same values in the same submitted order. */
|
|
524
|
+
#sameTags(left, right) {
|
|
525
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
526
|
+
}
|
|
291
527
|
};
|
|
292
528
|
|
|
293
529
|
export { TagsInputController };
|