stimeo-ui 0.14.0 → 0.16.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 +212 -0
- data/README.md +120 -0
- data/dist/cable/index.js +123 -29
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +96 -62
- data/dist/controllers/auto_submit_controller.js +83 -7
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +38 -11
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +340 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +81 -12
- data/dist/controllers/clipboard_controller.js +63 -5
- data/dist/controllers/collapsible_controller.js +99 -14
- data/dist/controllers/color_picker_controller.js +80 -34
- data/dist/controllers/combobox_controller.js +106 -16
- data/dist/controllers/command_palette_controller.js +35 -3
- data/dist/controllers/conditional_fields_controller.js +85 -17
- data/dist/controllers/confirm_controller.js +3 -0
- data/dist/controllers/context_menu_controller.js +32 -12
- data/dist/controllers/countdown_controller.js +129 -26
- data/dist/controllers/currency_input_controller.js +221 -67
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +151 -30
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +46 -13
- data/dist/controllers/dismissible_controller.js +1 -0
- data/dist/controllers/drawer_controller.js +54 -19
- data/dist/controllers/dropdown_controller.js +36 -9
- data/dist/controllers/editable_controller.js +34 -0
- data/dist/controllers/file_dropzone_controller.js +144 -51
- data/dist/controllers/filter_controller.js +20 -6
- data/dist/controllers/flash_controller.js +432 -71
- data/dist/controllers/focus_controller.js +1 -0
- data/dist/controllers/form_field_controller.js +7 -5
- data/dist/controllers/form_validation_controller.js +19 -13
- data/dist/controllers/frame_loading_controller.js +45 -8
- data/dist/controllers/highlight_controller.js +82 -25
- data/dist/controllers/hover_card_controller.js +40 -14
- data/dist/controllers/idle_controller.js +90 -5
- data/dist/controllers/input_mask_controller.js +65 -9
- data/dist/controllers/intersection_controller.js +3 -0
- data/dist/controllers/lazy_frame_controller.js +11 -2
- data/dist/controllers/listbox_controller.js +203 -45
- data/dist/controllers/local_time_controller.js +10 -5
- data/dist/controllers/masonry_controller.js +31 -15
- data/dist/controllers/menu_controller.js +45 -16
- data/dist/controllers/menubar_controller.js +58 -24
- data/dist/controllers/meter_controller.js +9 -5
- data/dist/controllers/multi_select_controller.js +278 -104
- data/dist/controllers/navigation_menu_controller.js +48 -15
- data/dist/controllers/nested_form_controller.js +37 -8
- data/dist/controllers/network_status_controller.js +9 -1
- data/dist/controllers/number_input_controller.js +124 -21
- data/dist/controllers/optimistic_controller.js +42 -5
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +115 -21
- data/dist/controllers/overflow_menu_controller.js +141 -44
- data/dist/controllers/pagination_controller.js +74 -28
- data/dist/controllers/password_reveal_controller.js +59 -2
- data/dist/controllers/persist_controller.js +30 -8
- data/dist/controllers/pointer_drag_controller.js +131 -52
- data/dist/controllers/popover_controller.js +45 -11
- data/dist/controllers/portal_controller.js +6 -2
- data/dist/controllers/preview_guard_controller.js +16 -1
- data/dist/controllers/progress_controller.js +8 -4
- data/dist/controllers/radio_group_controller.js +42 -17
- data/dist/controllers/range_slider_controller.js +88 -42
- data/dist/controllers/rating_controller.js +39 -15
- data/dist/controllers/read_more_controller.js +100 -7
- data/dist/controllers/reading_progress_controller.js +65 -19
- data/dist/controllers/relative_time_controller.js +10 -5
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +75 -27
- data/dist/controllers/scroll_restore_controller.js +37 -16
- data/dist/controllers/scroll_visibility_controller.js +49 -30
- data/dist/controllers/scrollspy_controller.js +71 -26
- data/dist/controllers/separator_controller.js +66 -37
- data/dist/controllers/sidebar_controller.js +77 -18
- data/dist/controllers/skeleton_controller.js +6 -1
- data/dist/controllers/slider_controller.js +82 -47
- data/dist/controllers/smart_sticky_header_controller.js +60 -26
- data/dist/controllers/sortable_controller.js +17 -2
- data/dist/controllers/spinner_controller.js +10 -2
- data/dist/controllers/step_indicator_controller.js +18 -17
- data/dist/controllers/stepper_controller.js +101 -19
- data/dist/controllers/stick_to_bottom_controller.js +104 -8
- data/dist/controllers/submit_once_controller.js +45 -9
- data/dist/controllers/switch_controller.js +101 -10
- data/dist/controllers/tabs_controller.js +21 -2
- data/dist/controllers/tags_input_controller.js +209 -59
- data/dist/controllers/textarea_autosize_controller.js +29 -3
- data/dist/controllers/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +451 -105
- data/dist/controllers/toggle_group_controller.js +159 -23
- data/dist/controllers/toolbar_controller.js +32 -0
- data/dist/controllers/tooltip_controller.js +39 -13
- data/dist/controllers/transition_controller.js +4 -0
- data/dist/controllers/tree_view_controller.js +169 -16
- data/dist/index.js +5002 -1911
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -2,6 +2,78 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/checkbox_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/form_reset_watcher.ts
|
|
6
|
+
var FormResetWatcher = class {
|
|
7
|
+
#owns;
|
|
8
|
+
#onReset;
|
|
9
|
+
#frames = /* @__PURE__ */ new Set();
|
|
10
|
+
#generation = 0;
|
|
11
|
+
/**
|
|
12
|
+
* @param owns - whether `form` owns a control this controller derives from.
|
|
13
|
+
* @param onReset - the reconciliation pass, run once per honoured reset.
|
|
14
|
+
*/
|
|
15
|
+
constructor(owns, onReset) {
|
|
16
|
+
this.#owns = owns;
|
|
17
|
+
this.#onReset = onReset;
|
|
18
|
+
}
|
|
19
|
+
/** Subscribes to `reset` at the document; call from `connect()`. Idempotent. */
|
|
20
|
+
observe() {
|
|
21
|
+
document.addEventListener("reset", this.#handleReset, true);
|
|
22
|
+
}
|
|
23
|
+
/** Unsubscribes and drops every pending pass; call from `disconnect()`. */
|
|
24
|
+
disconnect() {
|
|
25
|
+
document.removeEventListener("reset", this.#handleReset, true);
|
|
26
|
+
this.#generation += 1;
|
|
27
|
+
for (const frame of this.#frames) cancelAnimationFrame(frame);
|
|
28
|
+
this.#frames.clear();
|
|
29
|
+
}
|
|
30
|
+
#handleReset = (event) => {
|
|
31
|
+
const form = event.target;
|
|
32
|
+
if (!(form instanceof HTMLFormElement) || !this.#owns(form)) return;
|
|
33
|
+
const generation = this.#generation;
|
|
34
|
+
queueMicrotask(() => {
|
|
35
|
+
if (generation !== this.#generation) return;
|
|
36
|
+
if (event.eventPhase === Event.NONE) {
|
|
37
|
+
this.#settle(event);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const frame = requestAnimationFrame(() => {
|
|
41
|
+
this.#frames.delete(frame);
|
|
42
|
+
this.#settle(event);
|
|
43
|
+
});
|
|
44
|
+
this.#frames.add(frame);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
/** Runs the pass unless a listener cancelled the reset before it landed. */
|
|
48
|
+
#settle(event) {
|
|
49
|
+
if (!event.defaultPrevented) this.#onReset();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/utils/listener_set.ts
|
|
54
|
+
var ListenerSet = class {
|
|
55
|
+
/** The generation every `add` joins until the next `dispose()`. */
|
|
56
|
+
#abort = new AbortController();
|
|
57
|
+
/**
|
|
58
|
+
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
59
|
+
*
|
|
60
|
+
* The set supplies the signal, so `options` carries everything else the DOM
|
|
61
|
+
* accepts — `capture` included, which has to match at release time and no
|
|
62
|
+
* longer has a second place to drift from.
|
|
63
|
+
*/
|
|
64
|
+
add(target, type, handler, options) {
|
|
65
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Releases every listener of the open generation, synchronously, and opens the
|
|
69
|
+
* next one. Idempotent, and safe before anything has been added.
|
|
70
|
+
*/
|
|
71
|
+
dispose() {
|
|
72
|
+
this.#abort.abort();
|
|
73
|
+
this.#abort = new AbortController();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
5
77
|
// src/utils/microtask_coalescer.ts
|
|
6
78
|
var MicrotaskCoalescer = class {
|
|
7
79
|
#run;
|
|
@@ -42,6 +114,11 @@ var CheckboxController = class extends Controller {
|
|
|
42
114
|
static events = ["change", "reconcile"];
|
|
43
115
|
/** Collapses every lifecycle signal from one DOM update into one derived pass. */
|
|
44
116
|
#reconcile = new MicrotaskCoalescer(() => this.#reconcileFromChildren());
|
|
117
|
+
#listeners = new ListenerSet();
|
|
118
|
+
#formReset = new FormResetWatcher(
|
|
119
|
+
(form) => this.#hasCheckboxOwnedBy(form),
|
|
120
|
+
() => this.#reconcile.schedule()
|
|
121
|
+
);
|
|
45
122
|
/** Aggregate this root last settled on, so a derived repair is reported once. */
|
|
46
123
|
#committedState = null;
|
|
47
124
|
/** Watches authored checked-attribute changes on retained target elements. */
|
|
@@ -59,15 +136,15 @@ var CheckboxController = class extends Controller {
|
|
|
59
136
|
attributeFilter: ["checked"],
|
|
60
137
|
subtree: true
|
|
61
138
|
});
|
|
62
|
-
this.element
|
|
63
|
-
|
|
139
|
+
this.#listeners.add(this.element, "turbo:morph-element", this.#onMorph);
|
|
140
|
+
this.#formReset.observe();
|
|
64
141
|
}
|
|
65
142
|
/** Releases the observer, global reset listener, and every pending reconciliation. */
|
|
66
143
|
disconnect() {
|
|
67
144
|
this.#reconcile.cancel();
|
|
68
145
|
this.#checkedObserver.disconnect();
|
|
69
|
-
this.
|
|
70
|
-
|
|
146
|
+
this.#listeners.dispose();
|
|
147
|
+
this.#formReset.disconnect();
|
|
71
148
|
}
|
|
72
149
|
/** Reconciles the aggregate for a parent added or replaced at runtime. */
|
|
73
150
|
parentTargetConnected() {
|
|
@@ -145,14 +222,6 @@ var CheckboxController = class extends Controller {
|
|
|
145
222
|
#onMorph = () => {
|
|
146
223
|
this.#reconcile.schedule();
|
|
147
224
|
};
|
|
148
|
-
/** Reconciles after a non-cancelled reset restores any managed checkbox. */
|
|
149
|
-
#onReset = (event) => {
|
|
150
|
-
const form = event.target;
|
|
151
|
-
if (!(form instanceof HTMLFormElement) || !this.#hasCheckboxOwnedBy(form)) return;
|
|
152
|
-
queueMicrotask(() => {
|
|
153
|
-
if (!event.defaultPrevented) this.#reconcile.schedule();
|
|
154
|
-
});
|
|
155
|
-
};
|
|
156
225
|
/** Whether a form owns at least one current parent or child target. */
|
|
157
226
|
#hasCheckboxOwnedBy(form) {
|
|
158
227
|
return [...this.parentTargets, ...this.childTargets].some((checkbox) => checkbox.form === form);
|
|
@@ -127,7 +127,9 @@ var ClipboardController = class extends Controller {
|
|
|
127
127
|
* Whether this connection is still live. `copy()` suspends on the Clipboard API,
|
|
128
128
|
* and a teardown that lands while it is suspended must win: the continuation
|
|
129
129
|
* would otherwise write to an element nobody owns and arm a timer past the
|
|
130
|
-
* `clearAll()` that was supposed to be the last word.
|
|
130
|
+
* `clearAll()` that was supposed to be the last word. Stimulus also runs the label
|
|
131
|
+
* callbacks ahead of `connect()`, while this is still false: a result restored with
|
|
132
|
+
* the markup is `connect()`'s to settle, not theirs.
|
|
131
133
|
*/
|
|
132
134
|
#connected = false;
|
|
133
135
|
connect() {
|
|
@@ -140,10 +142,33 @@ var ClipboardController = class extends Controller {
|
|
|
140
142
|
this.#beforeCache.deactivate();
|
|
141
143
|
this.#timers.clearAll();
|
|
142
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Follows a `copiedLabel` swapped in place while a successful copy is shown.
|
|
147
|
+
*
|
|
148
|
+
* Render only: nothing is announced or dispatched, and the return to idle keeps the
|
|
149
|
+
* deadline the copy set. The slot changes only while `data-state` is `copied` and it
|
|
150
|
+
* still reads the previous label — the one the copy wrote there — so text someone
|
|
151
|
+
* else put in the slot stays. Stimulus also runs this ahead of `connect()`, which
|
|
152
|
+
* settles a result restored with the markup itself, so nothing is written then.
|
|
153
|
+
*/
|
|
154
|
+
copiedLabelValueChanged(label, previous) {
|
|
155
|
+
this.#followLabel("copied", label, previous);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Follows an `errorLabel` swapped in place while a failed copy is shown, on the same
|
|
159
|
+
* terms as {@link ClipboardController.copiedLabelValueChanged}: only while
|
|
160
|
+
* `data-state` is `error` and the slot still reads the previous label.
|
|
161
|
+
*/
|
|
162
|
+
errorLabelValueChanged(label, previous) {
|
|
163
|
+
this.#followLabel("error", label, previous);
|
|
164
|
+
}
|
|
143
165
|
/**
|
|
144
166
|
* Copies the resolved text and reports the outcome. Bound via `data-action`
|
|
145
|
-
* (click). Dispatches `stimeo--clipboard:copy` with `{ success, text }`
|
|
146
|
-
* completed attempt — including on failure — so consumers can react
|
|
167
|
+
* (click). Dispatches `stimeo--clipboard:copy` with `{ success, text, message }`
|
|
168
|
+
* once per completed attempt — including on failure — so consumers can react
|
|
169
|
+
* either way. `message` is the authored label for that outcome, so a part that
|
|
170
|
+
* shows wording — `stimeo--clipboard:copy->stimeo--toast#show` — needs nothing
|
|
171
|
+
* else; wiring both that and the announcer reads the same result twice.
|
|
147
172
|
* An attempt whose connection ended while it was in flight reports nothing.
|
|
148
173
|
*/
|
|
149
174
|
async copy() {
|
|
@@ -157,7 +182,8 @@ var ClipboardController = class extends Controller {
|
|
|
157
182
|
}
|
|
158
183
|
if (!this.#connected) return;
|
|
159
184
|
this.#reportResult(success);
|
|
160
|
-
|
|
185
|
+
const message = success ? this.copiedLabelValue : this.errorLabelValue;
|
|
186
|
+
this.dispatch("copy", { detail: { success, text, message } });
|
|
161
187
|
}
|
|
162
188
|
/**
|
|
163
189
|
* The text to copy: the explicit `text` value when set, otherwise the source
|
|
@@ -205,18 +231,50 @@ var ClipboardController = class extends Controller {
|
|
|
205
231
|
this.#timers.clearAll();
|
|
206
232
|
this.#reset();
|
|
207
233
|
}
|
|
208
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* Reflects the result, announces it, and schedules the return to idle.
|
|
236
|
+
*
|
|
237
|
+
* @stimeoRenderRoot
|
|
238
|
+
*/
|
|
209
239
|
#reportResult(success) {
|
|
210
240
|
this.element.setAttribute("data-state", success ? "copied" : "error");
|
|
211
241
|
if (this.hasFeedbackTarget) {
|
|
212
242
|
this.feedbackTarget.textContent = success ? this.copiedLabelValue : this.errorLabelValue;
|
|
213
243
|
}
|
|
244
|
+
this.#announceResult(success);
|
|
245
|
+
this.#scheduleReset();
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Reads the result out through the shared announcer.
|
|
249
|
+
*
|
|
250
|
+
* @stimeoRuntimeOnly `announceCopiedText` / `announceErrorText` word the one announcement a copy
|
|
251
|
+
* makes.
|
|
252
|
+
*/
|
|
253
|
+
#announceResult(success) {
|
|
214
254
|
announce(success ? this.announceCopiedTextValue : this.announceErrorTextValue);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Arms the return to idle for the result just shown.
|
|
258
|
+
*
|
|
259
|
+
* @stimeoRuntimeOnly `feedbackDuration` is the delay of the one timer this call arms, and the
|
|
260
|
+
* reset that timer runs reads no Value.
|
|
261
|
+
*/
|
|
262
|
+
#scheduleReset() {
|
|
215
263
|
this.#timers.clearAll();
|
|
216
264
|
if (this.feedbackDurationValue > 0) {
|
|
217
265
|
this.#timers.set(() => this.#reset(), this.feedbackDurationValue);
|
|
218
266
|
}
|
|
219
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Rewrites the slot to `label` while `state` is shown and the slot still reads
|
|
270
|
+
* `previous`, the label this controller wrote for that result.
|
|
271
|
+
*/
|
|
272
|
+
#followLabel(state, label, previous) {
|
|
273
|
+
if (!this.#connected) return;
|
|
274
|
+
if (this.element.getAttribute("data-state") !== state || !this.hasFeedbackTarget) return;
|
|
275
|
+
if (this.feedbackTarget.textContent !== previous) return;
|
|
276
|
+
this.feedbackTarget.textContent = label;
|
|
277
|
+
}
|
|
220
278
|
/** Returns to the idle state and empties the completion slot. */
|
|
221
279
|
#reset() {
|
|
222
280
|
this.element.setAttribute("data-state", "idle");
|
|
@@ -2,6 +2,57 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/collapsible_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/state_reason.ts
|
|
6
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
7
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
8
|
+
function stateReasonFor(event) {
|
|
9
|
+
if (!event) return "api";
|
|
10
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
11
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
12
|
+
return "user";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/utils/state_regions.ts
|
|
16
|
+
var StateRegions = class {
|
|
17
|
+
#whenTrue;
|
|
18
|
+
#whenFalse;
|
|
19
|
+
#taken = /* @__PURE__ */ new WeakSet();
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.#whenTrue = options.whenTrue;
|
|
22
|
+
this.#whenFalse = options.whenFalse ?? null;
|
|
23
|
+
}
|
|
24
|
+
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
25
|
+
reflect(host, isTrue) {
|
|
26
|
+
const shown = this.#inside(host, this.#whenTrue());
|
|
27
|
+
if (!this.#whenFalse) {
|
|
28
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const hiddenSide = this.#inside(host, this.#whenFalse());
|
|
32
|
+
if (shown.length === 0 || hiddenSide.length === 0) {
|
|
33
|
+
for (const region of shown) this.#give(region);
|
|
34
|
+
for (const region of hiddenSide) this.#give(region);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
38
|
+
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
39
|
+
}
|
|
40
|
+
/** Writes `hidden` where it moves, noting which regions it takes out of view. */
|
|
41
|
+
#write(region, hidden) {
|
|
42
|
+
if (hidden) this.#taken.add(region);
|
|
43
|
+
else this.#taken.delete(region);
|
|
44
|
+
if (region.hidden !== hidden) region.hidden = hidden;
|
|
45
|
+
}
|
|
46
|
+
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
47
|
+
#give(region) {
|
|
48
|
+
if (this.#taken.delete(region)) region.hidden = false;
|
|
49
|
+
}
|
|
50
|
+
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
51
|
+
#inside(host, regions) {
|
|
52
|
+
return regions.filter((region) => host.contains(region));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
5
56
|
// src/utils/safe_timeout.ts
|
|
6
57
|
var TimerRegistry = class {
|
|
7
58
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -198,15 +249,23 @@ var TransitionCompletion = class {
|
|
|
198
249
|
|
|
199
250
|
// src/controllers/collapsible_controller.ts
|
|
200
251
|
var CollapsibleController = class extends Controller {
|
|
201
|
-
static targets = ["trigger", "content"];
|
|
252
|
+
static targets = ["trigger", "content", "expandedLabel", "collapsedLabel"];
|
|
202
253
|
static values = {
|
|
203
254
|
open: { type: Boolean, default: false }
|
|
204
255
|
};
|
|
205
256
|
static actions = ["toggle"];
|
|
257
|
+
static events = ["close", "open"];
|
|
206
258
|
/** Owns the cancellable close-transition wait and its bounded fallback. */
|
|
207
259
|
#transition = new TransitionCompletion();
|
|
260
|
+
/** Owns `hidden` on the trigger's label pair, which follows `aria-expanded`. */
|
|
261
|
+
#labels = new StateRegions({
|
|
262
|
+
whenTrue: () => this.expandedLabelTargets,
|
|
263
|
+
whenFalse: () => this.collapsedLabelTargets
|
|
264
|
+
});
|
|
208
265
|
/** Distinguishes dynamic target churn from the callbacks that precede `connect()`. */
|
|
209
266
|
#connected = false;
|
|
267
|
+
/** Whether state moves are reported: set once `connect()` settled the baseline. */
|
|
268
|
+
#reporting = false;
|
|
210
269
|
/**
|
|
211
270
|
* Establishes the initial open/closed state without waiting for a close transition.
|
|
212
271
|
*
|
|
@@ -221,7 +280,8 @@ var CollapsibleController = class extends Controller {
|
|
|
221
280
|
*/
|
|
222
281
|
connect() {
|
|
223
282
|
this.#connected = true;
|
|
224
|
-
this.#apply(this.#initialOpen(), false);
|
|
283
|
+
this.#apply(this.#initialOpen(), false, "api");
|
|
284
|
+
this.#reporting = true;
|
|
225
285
|
}
|
|
226
286
|
/** Resolves the connect-time state: explicit DOM state wins, else the `open` Value. */
|
|
227
287
|
#initialOpen() {
|
|
@@ -238,15 +298,33 @@ var CollapsibleController = class extends Controller {
|
|
|
238
298
|
}
|
|
239
299
|
disconnect() {
|
|
240
300
|
this.#connected = false;
|
|
301
|
+
this.#reporting = false;
|
|
241
302
|
this.#transition.cancel();
|
|
242
303
|
}
|
|
243
304
|
/** Reconciles a replacement trigger target with the content's live state. */
|
|
244
305
|
triggerTargetConnected(trigger) {
|
|
245
|
-
if (!this.#connected
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
306
|
+
if (!this.#connected) return;
|
|
307
|
+
if (this.hasContentTarget) {
|
|
308
|
+
trigger.setAttribute(
|
|
309
|
+
"aria-expanded",
|
|
310
|
+
this.contentTarget.getAttribute("data-state") === "open" ? "true" : "false"
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
this.#labels.reflect(trigger, trigger.getAttribute("aria-expanded") === "true");
|
|
314
|
+
}
|
|
315
|
+
/** Settles a label that arrived after the state it belongs to was written. */
|
|
316
|
+
expandedLabelTargetConnected() {
|
|
317
|
+
this.#reflectLabels();
|
|
318
|
+
}
|
|
319
|
+
/** Settles a label that arrived after the state it belongs to was written. */
|
|
320
|
+
collapsedLabelTargetConnected() {
|
|
321
|
+
this.#reflectLabels();
|
|
322
|
+
}
|
|
323
|
+
/** Shows the label pair's side that belongs to the trigger's expanded state. */
|
|
324
|
+
#reflectLabels() {
|
|
325
|
+
if (this.#connected && this.hasTriggerTarget) {
|
|
326
|
+
this.#labels.reflect(this.triggerTarget, this.#isOpen);
|
|
327
|
+
}
|
|
250
328
|
}
|
|
251
329
|
/** Reconciles a replacement content target with the disclosure's live state. */
|
|
252
330
|
contentTargetConnected(content) {
|
|
@@ -260,8 +338,8 @@ var CollapsibleController = class extends Controller {
|
|
|
260
338
|
this.#transition.cancel();
|
|
261
339
|
}
|
|
262
340
|
/** Toggles the region open/closed. Bound via `data-action` (click). */
|
|
263
|
-
toggle() {
|
|
264
|
-
this.#apply(!this.#isOpen, true);
|
|
341
|
+
toggle(event) {
|
|
342
|
+
this.#apply(!this.#isOpen, true, stateReasonFor(event));
|
|
265
343
|
}
|
|
266
344
|
/**
|
|
267
345
|
* Whether the region is logically open. Read from `aria-expanded` (not the
|
|
@@ -283,15 +361,22 @@ var CollapsibleController = class extends Controller {
|
|
|
283
361
|
* @param waitForCloseTransition - When `false` (initial `connect`) the close
|
|
284
362
|
* path applies `hidden` immediately. This flag does not suppress consumer CSS
|
|
285
363
|
* on the open path.
|
|
364
|
+
* @param reason - What drove the move, reported when the state actually moved.
|
|
286
365
|
*/
|
|
287
|
-
#apply(open, waitForCloseTransition) {
|
|
366
|
+
#apply(open, waitForCloseTransition, reason) {
|
|
367
|
+
const was = this.#isOpen;
|
|
288
368
|
if (this.hasTriggerTarget) {
|
|
289
369
|
this.triggerTarget.setAttribute("aria-expanded", open ? "true" : "false");
|
|
370
|
+
this.#labels.reflect(this.triggerTarget, open);
|
|
290
371
|
}
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
372
|
+
if (this.hasContentTarget) {
|
|
373
|
+
const content = this.contentTarget;
|
|
374
|
+
this.#transition.cancel();
|
|
375
|
+
this.#applyContent(content, open, waitForCloseTransition);
|
|
376
|
+
}
|
|
377
|
+
if (was === open || !this.#reporting) return;
|
|
378
|
+
if (open) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
379
|
+
else this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
295
380
|
}
|
|
296
381
|
/** Reflects one content target without relying on a later target lookup. */
|
|
297
382
|
#applyContent(content, open, waitForCloseTransition) {
|
|
@@ -25,6 +25,16 @@ function toFiniteNumber(raw) {
|
|
|
25
25
|
return Number.isFinite(value) ? value : null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// src/utils/field_mirror.ts
|
|
29
|
+
function writeField(field, value) {
|
|
30
|
+
if (field.value === value) return false;
|
|
31
|
+
field.value = value;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function commitField(target) {
|
|
35
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
// src/utils/microtask_coalescer.ts
|
|
29
39
|
var MicrotaskCoalescer = class {
|
|
30
40
|
#run;
|
|
@@ -60,54 +70,46 @@ var MicrotaskCoalescer = class {
|
|
|
60
70
|
|
|
61
71
|
// src/utils/owned_pointer_session.ts
|
|
62
72
|
var OwnedPointerSession = class {
|
|
63
|
-
pointerId;
|
|
73
|
+
#pointerId;
|
|
64
74
|
#owner;
|
|
65
75
|
#handlers;
|
|
66
76
|
#abort = new AbortController();
|
|
67
77
|
#active = true;
|
|
68
78
|
constructor(start, owner, handlers) {
|
|
69
|
-
this
|
|
79
|
+
this.#pointerId = start.pointerId;
|
|
70
80
|
this.#owner = owner;
|
|
71
81
|
this.#handlers = handlers;
|
|
72
82
|
const { signal } = this.#abort;
|
|
73
83
|
owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
|
|
74
84
|
owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
|
|
75
85
|
owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
|
|
76
|
-
owner.addEventListener("lostpointercapture", this.#onLostCapture, { signal });
|
|
77
86
|
try {
|
|
78
|
-
owner.setPointerCapture?.(this
|
|
87
|
+
owner.setPointerCapture?.(this.#pointerId);
|
|
79
88
|
} catch {
|
|
80
89
|
}
|
|
81
90
|
}
|
|
82
|
-
/**
|
|
83
|
-
get active() {
|
|
84
|
-
return this.#active;
|
|
85
|
-
}
|
|
86
|
-
/** Whether `event` belongs to the initiating pointer of the live session. */
|
|
87
|
-
owns(event) {
|
|
88
|
-
return this.#active && event.pointerId === this.pointerId;
|
|
89
|
-
}
|
|
90
|
-
/** Releases capture/listeners and invokes the end callback exactly once. */
|
|
91
|
+
/** Releases capture and listeners, then reports the end exactly once. */
|
|
91
92
|
end() {
|
|
93
|
+
this.#finish("teardown");
|
|
94
|
+
}
|
|
95
|
+
#finish(kind) {
|
|
92
96
|
if (!this.#active) return;
|
|
93
97
|
this.#active = false;
|
|
94
98
|
this.#abort.abort();
|
|
95
99
|
try {
|
|
96
|
-
this.#owner.releasePointerCapture?.(this
|
|
100
|
+
this.#owner.releasePointerCapture?.(this.#pointerId);
|
|
97
101
|
} catch {
|
|
98
102
|
}
|
|
99
|
-
this.#handlers.end?.();
|
|
103
|
+
this.#handlers.end?.(kind);
|
|
104
|
+
}
|
|
105
|
+
#owns(event) {
|
|
106
|
+
return this.#active && event.pointerId === this.#pointerId;
|
|
100
107
|
}
|
|
101
108
|
#onMove = (event) => {
|
|
102
|
-
if (this
|
|
109
|
+
if (this.#owns(event)) this.#handlers.move(event);
|
|
103
110
|
};
|
|
104
111
|
#onEndEvent = (event) => {
|
|
105
|
-
if (this
|
|
106
|
-
};
|
|
107
|
-
#onLostCapture = (event) => {
|
|
108
|
-
const pointerId = event.pointerId;
|
|
109
|
-
if (typeof pointerId === "number" && pointerId !== this.pointerId) return;
|
|
110
|
-
this.end();
|
|
112
|
+
if (this.#owns(event)) this.#finish(event.type === "pointerup" ? "up" : "cancel");
|
|
111
113
|
};
|
|
112
114
|
};
|
|
113
115
|
|
|
@@ -139,6 +141,19 @@ var ColorPickerController = class extends Controller {
|
|
|
139
141
|
#drag = null;
|
|
140
142
|
/** Color the last repaint settled on, so a configuration-driven move is reported once. */
|
|
141
143
|
#committedHex = null;
|
|
144
|
+
/**
|
|
145
|
+
* Whether the paint about to run was asked for by this picker's own controls.
|
|
146
|
+
* The `value` Value is shared with the page — application code and a Turbo
|
|
147
|
+
* morph write it too — so the form fields and the hex input take their "did
|
|
148
|
+
* the user commit this" answer from the route, not from the Value.
|
|
149
|
+
*/
|
|
150
|
+
#movedByUser = false;
|
|
151
|
+
/**
|
|
152
|
+
* The hex this picker last wrote into its hex input, or `null` before it wrote
|
|
153
|
+
* one. A repaint the page drives compares with it rather than with the input's
|
|
154
|
+
* text, so a color that did not move leaves what the reader is typing alone.
|
|
155
|
+
*/
|
|
156
|
+
#writtenHex = null;
|
|
142
157
|
/**
|
|
143
158
|
* Collapses a morph that swaps render inputs into one repaint, and refuses the
|
|
144
159
|
* pass Stimulus delivers before `connect()`.
|
|
@@ -164,9 +179,14 @@ var ColorPickerController = class extends Controller {
|
|
|
164
179
|
if (this.valueValue === this.#committedHex) return;
|
|
165
180
|
this.#repaint.schedule();
|
|
166
181
|
}
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Hydrates a channel slider inserted or replaced at runtime through the repaint
|
|
184
|
+
* pass, which renders every slider after the batch and reports a committed
|
|
185
|
+
* color that moved. A slider arriving moves no color, so that pass reports
|
|
186
|
+
* nothing.
|
|
187
|
+
*/
|
|
188
|
+
sliderTargetConnected() {
|
|
189
|
+
this.#repaint.schedule();
|
|
170
190
|
}
|
|
171
191
|
/** Ends a gesture whose geometry target disappeared or ceased being a target. */
|
|
172
192
|
sliderTargetDisconnected(slider) {
|
|
@@ -174,7 +194,7 @@ var ColorPickerController = class extends Controller {
|
|
|
174
194
|
}
|
|
175
195
|
/** Fills a hex input inserted or replaced at runtime with the current color. */
|
|
176
196
|
hexTargetConnected(hex) {
|
|
177
|
-
this.#
|
|
197
|
+
this.#writeHex(hex, this.#hexString());
|
|
178
198
|
}
|
|
179
199
|
/** Fills a form field inserted or replaced at runtime with the current color. */
|
|
180
200
|
fieldTargetConnected(field) {
|
|
@@ -256,7 +276,7 @@ var ColorPickerController = class extends Controller {
|
|
|
256
276
|
if (!this.hasHexTarget) return;
|
|
257
277
|
const parsed = hexToHsla(this.hexTarget.value);
|
|
258
278
|
if (!parsed) {
|
|
259
|
-
this.hexTarget
|
|
279
|
+
this.#writeHex(this.hexTarget, this.#hexString());
|
|
260
280
|
return;
|
|
261
281
|
}
|
|
262
282
|
this.#color = this.#opaqueUnlessEnabled(parsed);
|
|
@@ -284,27 +304,44 @@ var ColorPickerController = class extends Controller {
|
|
|
284
304
|
* Renders the model and reports a color the user actually moved. A key pressed
|
|
285
305
|
* at a bound, a pointer that lands on the step already showing, and a re-confirmed
|
|
286
306
|
* hex all leave the committed color where it was, so no `change` describes them.
|
|
307
|
+
*
|
|
308
|
+
* The color is written into `value` first — the one path that writes it — so a
|
|
309
|
+
* Turbo snapshot and a morph read the color the user picked, and a listener of
|
|
310
|
+
* the field's native `change` already finds it there.
|
|
287
311
|
*/
|
|
288
312
|
#commitColor() {
|
|
289
313
|
const previous = this.#committedHex;
|
|
314
|
+
const hex = this.#hexString();
|
|
315
|
+
if (this.valueValue !== hex) this.valueValue = hex;
|
|
316
|
+
this.#movedByUser = true;
|
|
290
317
|
this.#render();
|
|
291
318
|
if (this.#committedHex !== previous) {
|
|
292
319
|
this.dispatch("change", { detail: this.#settledDetail() });
|
|
293
320
|
}
|
|
294
321
|
}
|
|
295
322
|
/**
|
|
296
|
-
* Reflects the model onto sliders, the hex input, preview, form field
|
|
297
|
-
* `value` Value it
|
|
323
|
+
* Reflects the model onto sliders, the hex input, preview, and form field. The
|
|
324
|
+
* `value` Value is left as it is.
|
|
325
|
+
*
|
|
326
|
+
* The hex input is written for the reader's own commit, which shows the color
|
|
327
|
+
* in its canonical form, and otherwise only when the hex it shows moved: a
|
|
328
|
+
* repaint the page drives that leaves the color where it is keeps text the
|
|
329
|
+
* reader has typed there and not committed.
|
|
298
330
|
*
|
|
299
331
|
* @stimeoRenderRoot
|
|
300
332
|
*/
|
|
301
333
|
#render() {
|
|
334
|
+
const byUser = this.#movedByUser;
|
|
335
|
+
this.#movedByUser = false;
|
|
302
336
|
for (const slider of this.sliderTargets) this.#renderSlider(slider);
|
|
303
337
|
const hex = this.#hexString();
|
|
304
338
|
this.#committedHex = hex;
|
|
305
|
-
if (this.
|
|
306
|
-
|
|
307
|
-
|
|
339
|
+
if (this.hasHexTarget && (byUser || hex !== this.#writtenHex)) {
|
|
340
|
+
this.#writeHex(this.hexTarget, hex);
|
|
341
|
+
}
|
|
342
|
+
for (const field of this.fieldTargets) {
|
|
343
|
+
if (this.#mirrorColor(field, hex) && byUser) commitField(field);
|
|
344
|
+
}
|
|
308
345
|
for (const preview of this.previewTargets) this.#publishColor(preview, hex);
|
|
309
346
|
this.#publishColor(this.element, hex);
|
|
310
347
|
}
|
|
@@ -324,9 +361,18 @@ var ColorPickerController = class extends Controller {
|
|
|
324
361
|
if (slider.getAttribute(name) !== next) slider.setAttribute(name, next);
|
|
325
362
|
}
|
|
326
363
|
}
|
|
327
|
-
/**
|
|
364
|
+
/** Writes `hex` into a hex input and keeps it as the hex last written there. */
|
|
365
|
+
#writeHex(input, hex) {
|
|
366
|
+
this.#writtenHex = hex;
|
|
367
|
+
this.#mirrorColor(input, hex);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Mirrors the color into an input, leaving an already-equal value untouched.
|
|
371
|
+
*
|
|
372
|
+
* @returns Whether the input's value moved.
|
|
373
|
+
*/
|
|
328
374
|
#mirrorColor(input, hex) {
|
|
329
|
-
|
|
375
|
+
return writeField(input, hex);
|
|
330
376
|
}
|
|
331
377
|
/** Publishes the color as the consumer's CSS hook, skipping an equal value. */
|
|
332
378
|
#publishColor(element, hex) {
|