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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +184 -0
- data/dist/controllers/aspect_ratio_controller.js +19 -11
- data/dist/controllers/avatar_controller.js +195 -40
- data/dist/controllers/breadcrumb_controller.js +5 -1
- data/dist/controllers/carousel_controller.js +90 -10
- data/dist/controllers/checkbox_controller.js +136 -25
- data/dist/controllers/clipboard_controller.js +8 -3
- data/dist/controllers/collapsible_controller.js +4 -1
- data/dist/controllers/color_picker_controller.js +41 -11
- data/dist/controllers/context_menu_controller.js +2 -2
- data/dist/controllers/countdown_controller.js +5 -1
- data/dist/controllers/date_range_picker_controller.js +162 -31
- data/dist/controllers/direct_upload_controller.js +3 -3
- data/dist/controllers/empty_state_controller.js +107 -16
- data/dist/controllers/file_dropzone_controller.js +26 -3
- data/dist/controllers/flash_controller.js +161 -21
- data/dist/controllers/form_validation_controller.js +8 -2
- data/dist/controllers/frame_loading_controller.js +94 -22
- data/dist/controllers/highlight_controller.js +38 -1
- data/dist/controllers/idle_controller.js +39 -6
- data/dist/controllers/local_time_controller.js +2 -0
- data/dist/controllers/masonry_controller.js +1 -1
- data/dist/controllers/menubar_controller.js +5 -3
- data/dist/controllers/meter_controller.js +3 -1
- data/dist/controllers/multi_select_controller.js +460 -151
- data/dist/controllers/network_status_controller.js +1 -3
- data/dist/controllers/number_input_controller.js +455 -64
- data/dist/controllers/overflow_menu_controller.js +5 -1
- data/dist/controllers/pagination_controller.js +38 -1
- data/dist/controllers/password_strength_controller.js +21 -3
- data/dist/controllers/persist_controller.js +24 -5
- data/dist/controllers/pointer_drag_controller.js +10 -0
- data/dist/controllers/portal_controller.js +10 -0
- data/dist/controllers/progress_controller.js +7 -3
- data/dist/controllers/radio_group_controller.js +540 -56
- data/dist/controllers/range_slider_controller.js +385 -94
- data/dist/controllers/rating_controller.js +274 -89
- data/dist/controllers/relative_time_controller.js +2 -0
- data/dist/controllers/resizable_controller.js +33 -0
- data/dist/controllers/roving_controller.js +60 -5
- data/dist/controllers/scroll_area_controller.js +155 -23
- data/dist/controllers/scroll_visibility_controller.js +33 -0
- data/dist/controllers/separator_controller.js +13 -17
- data/dist/controllers/skeleton_controller.js +71 -3
- data/dist/controllers/slider_controller.js +325 -48
- data/dist/controllers/spinner_controller.js +18 -3
- data/dist/controllers/step_indicator_controller.js +3 -1
- data/dist/controllers/stepper_controller.js +2 -0
- data/dist/controllers/switch_controller.js +162 -18
- data/dist/controllers/tags_input_controller.js +356 -120
- data/dist/controllers/textarea_autosize_controller.js +1 -1
- data/dist/controllers/time_picker_controller.js +296 -104
- data/dist/controllers/toggle_group_controller.js +378 -55
- data/dist/controllers/toolbar_controller.js +5 -3
- data/dist/controllers/tree_view_controller.js +24 -4
- data/dist/index.js +3811 -1048
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -20,6 +20,39 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
20
20
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// src/utils/microtask_coalescer.ts
|
|
24
|
+
var MicrotaskCoalescer = class {
|
|
25
|
+
#run;
|
|
26
|
+
#queued = false;
|
|
27
|
+
#active = false;
|
|
28
|
+
#generation = 0;
|
|
29
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
30
|
+
constructor(run) {
|
|
31
|
+
this.#run = run;
|
|
32
|
+
}
|
|
33
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
34
|
+
activate() {
|
|
35
|
+
this.#active = true;
|
|
36
|
+
}
|
|
37
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
38
|
+
cancel() {
|
|
39
|
+
this.#active = false;
|
|
40
|
+
this.#queued = false;
|
|
41
|
+
this.#generation += 1;
|
|
42
|
+
}
|
|
43
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
44
|
+
schedule() {
|
|
45
|
+
if (!this.#active || this.#queued) return;
|
|
46
|
+
this.#queued = true;
|
|
47
|
+
const generation = this.#generation;
|
|
48
|
+
queueMicrotask(() => {
|
|
49
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
50
|
+
this.#queued = false;
|
|
51
|
+
this.#run();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
23
56
|
// src/utils/roving_tabindex.ts
|
|
24
57
|
var RovingTabindex = class {
|
|
25
58
|
/** Returns the current ordered item elements; called on every operation. */
|
|
@@ -42,10 +75,12 @@ var RovingTabindex = class {
|
|
|
42
75
|
* "nothing is currently tabbable".
|
|
43
76
|
*
|
|
44
77
|
* @param index - Position of the item to make tabbable.
|
|
45
|
-
* @param options - Pass `{ focus: true }` to also move DOM focus to that item
|
|
78
|
+
* @param options - Pass `{ focus: true }` to also move DOM focus to that item,
|
|
79
|
+
* and `items` to reuse an event-scoped collection snapshot.
|
|
46
80
|
*/
|
|
47
|
-
setActive(index,
|
|
48
|
-
const
|
|
81
|
+
setActive(index, options = {}) {
|
|
82
|
+
const { focus = false } = options;
|
|
83
|
+
const items = options.items ?? this.#getItems();
|
|
49
84
|
items.forEach((item, i) => {
|
|
50
85
|
item.tabIndex = i === index ? 0 : -1;
|
|
51
86
|
});
|
|
@@ -120,9 +155,14 @@ var CarouselController = class extends Controller {
|
|
|
120
155
|
"resume",
|
|
121
156
|
"togglePlay"
|
|
122
157
|
];
|
|
123
|
-
static events = ["change", "pause", "play"];
|
|
158
|
+
static events = ["change", "pause", "play", "reconcile"];
|
|
124
159
|
#roving = new RovingTabindex(() => this.pickerTargets);
|
|
125
|
-
|
|
160
|
+
#reconcileTargets = new MicrotaskCoalescer(() => this.#reconcileTargetSet());
|
|
161
|
+
/**
|
|
162
|
+
* Whether `connect()` has run. Scheduling is already inert outside that window
|
|
163
|
+
* ({@link MicrotaskCoalescer}), so this only gates the Tab stop a picker
|
|
164
|
+
* present at mount authored for itself.
|
|
165
|
+
*/
|
|
126
166
|
#connected = false;
|
|
127
167
|
#intervals = new SafeInterval();
|
|
128
168
|
/** Index of the visible slide. */
|
|
@@ -148,6 +188,7 @@ var CarouselController = class extends Controller {
|
|
|
148
188
|
this.#render({ focus: false });
|
|
149
189
|
this.#syncTimer();
|
|
150
190
|
this.#connected = true;
|
|
191
|
+
this.#reconcileTargets.activate();
|
|
151
192
|
}
|
|
152
193
|
/**
|
|
153
194
|
* Re-establishes the single selected picker when one is added after connect.
|
|
@@ -158,9 +199,22 @@ var CarouselController = class extends Controller {
|
|
|
158
199
|
* repaint re-derives every picker from `#index`, so a late arrival never steals
|
|
159
200
|
* the selection.
|
|
160
201
|
*/
|
|
161
|
-
pickerTargetConnected() {
|
|
202
|
+
pickerTargetConnected(picker) {
|
|
162
203
|
if (!this.#connected) return;
|
|
163
|
-
|
|
204
|
+
picker.tabIndex = -1;
|
|
205
|
+
this.#reconcileTargets.schedule();
|
|
206
|
+
}
|
|
207
|
+
/** Repairs selection and roving after a picker leaves a retained carousel. */
|
|
208
|
+
pickerTargetDisconnected() {
|
|
209
|
+
this.#reconcileTargets.schedule();
|
|
210
|
+
}
|
|
211
|
+
/** Reconciles a slide added in the same DOM batch as its picker. */
|
|
212
|
+
slideTargetConnected() {
|
|
213
|
+
this.#reconcileTargets.schedule();
|
|
214
|
+
}
|
|
215
|
+
/** Re-clamps the active index after a slide is removed. */
|
|
216
|
+
slideTargetDisconnected() {
|
|
217
|
+
this.#reconcileTargets.schedule();
|
|
164
218
|
}
|
|
165
219
|
/**
|
|
166
220
|
* Resolves the starting autoplay intent. The play toggle's `aria-pressed` is the
|
|
@@ -178,6 +232,7 @@ var CarouselController = class extends Controller {
|
|
|
178
232
|
/** Clears the autoplay interval so it never fires after teardown. */
|
|
179
233
|
disconnect() {
|
|
180
234
|
this.#connected = false;
|
|
235
|
+
this.#reconcileTargets.cancel();
|
|
181
236
|
this.#intervals.clearAll();
|
|
182
237
|
this.#timerId = null;
|
|
183
238
|
}
|
|
@@ -271,17 +326,42 @@ var CarouselController = class extends Controller {
|
|
|
271
326
|
this.#syncTimer();
|
|
272
327
|
if (changed) this.dispatch("change", { detail: { index, total: this.slideTargets.length } });
|
|
273
328
|
}
|
|
274
|
-
/**
|
|
329
|
+
/**
|
|
330
|
+
* Reflects `this.#index` onto slides and pickers (state hooks + roving).
|
|
331
|
+
*
|
|
332
|
+
* @stimeoRenderRoot
|
|
333
|
+
*/
|
|
275
334
|
#render({ focus }) {
|
|
276
335
|
this.slideTargets.forEach((slide, i) => {
|
|
277
336
|
const active = i === this.#index;
|
|
278
337
|
slide.setAttribute("data-state", active ? "active" : "inactive");
|
|
279
338
|
slide.hidden = !active;
|
|
280
339
|
});
|
|
340
|
+
const pickerIndex = Math.min(this.#index, this.pickerTargets.length - 1);
|
|
281
341
|
this.pickerTargets.forEach((picker, i) => {
|
|
282
|
-
picker.setAttribute("aria-selected", i ===
|
|
342
|
+
picker.setAttribute("aria-selected", i === pickerIndex ? "true" : "false");
|
|
283
343
|
});
|
|
284
|
-
this.#roving.setActive(
|
|
344
|
+
this.#roving.setActive(pickerIndex, { focus });
|
|
345
|
+
}
|
|
346
|
+
/** Keeps the live active slide when possible and otherwise selects the nearest survivor. */
|
|
347
|
+
#reconcileTargetSet() {
|
|
348
|
+
const activeSlide = this.slideTargets.findIndex(
|
|
349
|
+
(slide) => slide.getAttribute("data-state") === "active"
|
|
350
|
+
);
|
|
351
|
+
const selectedPicker = this.pickerTargets.findIndex(
|
|
352
|
+
(picker) => picker.getAttribute("aria-selected") === "true"
|
|
353
|
+
);
|
|
354
|
+
const lastSlide = this.slideTargets.length - 1;
|
|
355
|
+
const candidate = activeSlide !== -1 ? activeSlide : selectedPicker !== -1 ? selectedPicker : this.#index;
|
|
356
|
+
const previous = this.#index;
|
|
357
|
+
this.#index = lastSlide < 0 ? 0 : Math.min(lastSlide, Math.max(0, candidate));
|
|
358
|
+
this.#render({ focus: false });
|
|
359
|
+
this.#syncTimer();
|
|
360
|
+
if (this.#index !== previous) {
|
|
361
|
+
this.dispatch("reconcile", {
|
|
362
|
+
detail: { index: this.#index, total: this.slideTargets.length }
|
|
363
|
+
});
|
|
364
|
+
}
|
|
285
365
|
}
|
|
286
366
|
/**
|
|
287
367
|
* Drives the autoplay interval toward the desired state. Autoplay should run
|
|
@@ -1,17 +1,89 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
// src/controllers/checkbox_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/microtask_coalescer.ts
|
|
6
|
+
var MicrotaskCoalescer = class {
|
|
7
|
+
#run;
|
|
8
|
+
#queued = false;
|
|
9
|
+
#active = false;
|
|
10
|
+
#generation = 0;
|
|
11
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
12
|
+
constructor(run) {
|
|
13
|
+
this.#run = run;
|
|
14
|
+
}
|
|
15
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
16
|
+
activate() {
|
|
17
|
+
this.#active = true;
|
|
18
|
+
}
|
|
19
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
20
|
+
cancel() {
|
|
21
|
+
this.#active = false;
|
|
22
|
+
this.#queued = false;
|
|
23
|
+
this.#generation += 1;
|
|
24
|
+
}
|
|
25
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
26
|
+
schedule() {
|
|
27
|
+
if (!this.#active || this.#queued) return;
|
|
28
|
+
this.#queued = true;
|
|
29
|
+
const generation = this.#generation;
|
|
30
|
+
queueMicrotask(() => {
|
|
31
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
32
|
+
this.#queued = false;
|
|
33
|
+
this.#run();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
3
38
|
// src/controllers/checkbox_controller.ts
|
|
4
39
|
var CheckboxController = class extends Controller {
|
|
5
40
|
static targets = ["parent", "child"];
|
|
6
41
|
static actions = ["onChildChange", "onParentChange"];
|
|
7
|
-
static events = ["change"];
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
42
|
+
static events = ["change", "reconcile"];
|
|
43
|
+
/** Collapses every lifecycle signal from one DOM update into one derived pass. */
|
|
44
|
+
#reconcile = new MicrotaskCoalescer(() => this.#reconcileFromChildren());
|
|
45
|
+
/** Aggregate this root last settled on, so a derived repair is reported once. */
|
|
46
|
+
#committedState = null;
|
|
47
|
+
/** Watches authored checked-attribute changes on retained target elements. */
|
|
48
|
+
#checkedObserver = new MutationObserver((records) => {
|
|
49
|
+
if (records.some((record) => this.#isManagedCheckbox(record.target))) {
|
|
50
|
+
this.#reconcile.schedule();
|
|
14
51
|
}
|
|
52
|
+
});
|
|
53
|
+
/** Reflects the initial aggregate and starts retained-DOM reconciliation. */
|
|
54
|
+
connect() {
|
|
55
|
+
this.#reconcile.activate();
|
|
56
|
+
this.#syncFromChildren();
|
|
57
|
+
this.#checkedObserver.observe(this.element, {
|
|
58
|
+
attributes: true,
|
|
59
|
+
attributeFilter: ["checked"],
|
|
60
|
+
subtree: true
|
|
61
|
+
});
|
|
62
|
+
this.element.addEventListener("turbo:morph-element", this.#onMorph);
|
|
63
|
+
document.addEventListener("reset", this.#onReset, true);
|
|
64
|
+
}
|
|
65
|
+
/** Releases the observer, global reset listener, and every pending reconciliation. */
|
|
66
|
+
disconnect() {
|
|
67
|
+
this.#reconcile.cancel();
|
|
68
|
+
this.#checkedObserver.disconnect();
|
|
69
|
+
this.element.removeEventListener("turbo:morph-element", this.#onMorph);
|
|
70
|
+
document.removeEventListener("reset", this.#onReset, true);
|
|
71
|
+
}
|
|
72
|
+
/** Reconciles the aggregate for a parent added or replaced at runtime. */
|
|
73
|
+
parentTargetConnected() {
|
|
74
|
+
this.#reconcile.schedule();
|
|
75
|
+
}
|
|
76
|
+
/** Reconciles the aggregate after a parent target leaves the group. */
|
|
77
|
+
parentTargetDisconnected() {
|
|
78
|
+
this.#reconcile.schedule();
|
|
79
|
+
}
|
|
80
|
+
/** Reconciles the aggregate for a child added at runtime. */
|
|
81
|
+
childTargetConnected() {
|
|
82
|
+
this.#reconcile.schedule();
|
|
83
|
+
}
|
|
84
|
+
/** Reconciles the aggregate after a child leaves the group. */
|
|
85
|
+
childTargetDisconnected() {
|
|
86
|
+
this.#reconcile.schedule();
|
|
15
87
|
}
|
|
16
88
|
/** Cascades the parent's state to every child. Bound via `data-action` (change). */
|
|
17
89
|
onParentChange() {
|
|
@@ -20,35 +92,74 @@ var CheckboxController = class extends Controller {
|
|
|
20
92
|
for (const child of this.childTargets) {
|
|
21
93
|
child.checked = checked;
|
|
22
94
|
}
|
|
23
|
-
this
|
|
24
|
-
const
|
|
25
|
-
this.
|
|
26
|
-
this.dispatch("change", { detail: { checked, indeterminate: false, state } });
|
|
95
|
+
this.#reflect(checked ? "all" : "none", true);
|
|
96
|
+
const detail = this.#settledDetail();
|
|
97
|
+
if (detail) this.dispatch("change", { detail });
|
|
27
98
|
}
|
|
28
99
|
/** Recomputes the parent from its children. Bound via `data-action` (change). */
|
|
29
100
|
onChildChange() {
|
|
30
|
-
this.#syncFromChildren(
|
|
101
|
+
this.#syncFromChildren();
|
|
102
|
+
const detail = this.#settledDetail();
|
|
103
|
+
if (detail) this.dispatch("change", { detail });
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Announces an aggregate this pass derived rather than the user. `change` stays
|
|
107
|
+
* reserved for the two public actions, so automation never reads a repair as an edit.
|
|
108
|
+
*/
|
|
109
|
+
#reconcileFromChildren() {
|
|
110
|
+
const previous = this.#committedState;
|
|
111
|
+
this.#syncFromChildren();
|
|
112
|
+
if (this.#committedState === previous) return;
|
|
113
|
+
const detail = this.#settledDetail();
|
|
114
|
+
if (detail) this.dispatch("reconcile", { detail });
|
|
31
115
|
}
|
|
32
116
|
/**
|
|
33
117
|
* Derives the parent's `checked`/`indeterminate` and the root `data-state` from
|
|
34
|
-
* the children
|
|
118
|
+
* the children. Writing state and reporting it are separate so the caller — not
|
|
119
|
+
* a flag threaded through the write — decides which event describes the cause.
|
|
35
120
|
*/
|
|
36
|
-
#syncFromChildren(
|
|
37
|
-
|
|
38
|
-
|
|
121
|
+
#syncFromChildren() {
|
|
122
|
+
this.#reflect(this.#aggregate(), this.childTargets.length > 0);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Reflects one aggregate state.
|
|
126
|
+
*
|
|
127
|
+
* @param writeParent - whether the state is authoritative over the `parent`
|
|
128
|
+
* target's own `checked` / `indeterminate`.
|
|
129
|
+
*/
|
|
130
|
+
#reflect(state, writeParent) {
|
|
131
|
+
if (writeParent && this.hasParentTarget) {
|
|
39
132
|
this.parentTarget.checked = state === "all";
|
|
40
133
|
this.parentTarget.indeterminate = state === "partial";
|
|
41
134
|
}
|
|
42
135
|
this.element.setAttribute("data-state", state);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
136
|
+
this.#committedState = state;
|
|
137
|
+
}
|
|
138
|
+
/** The settled aggregate as event detail, or `null` before anything has settled. */
|
|
139
|
+
#settledDetail() {
|
|
140
|
+
const state = this.#committedState;
|
|
141
|
+
if (state === null) return null;
|
|
142
|
+
return { checked: state === "all", indeterminate: state === "partial", state };
|
|
143
|
+
}
|
|
144
|
+
/** Reconciles retained targets after Turbo has finished morphing their live state. */
|
|
145
|
+
#onMorph = () => {
|
|
146
|
+
this.#reconcile.schedule();
|
|
147
|
+
};
|
|
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
|
+
/** Whether a form owns at least one current parent or child target. */
|
|
157
|
+
#hasCheckboxOwnedBy(form) {
|
|
158
|
+
return [...this.parentTargets, ...this.childTargets].some((checkbox) => checkbox.form === form);
|
|
159
|
+
}
|
|
160
|
+
/** Whether an observed attribute mutation belongs to this controller's target set. */
|
|
161
|
+
#isManagedCheckbox(node) {
|
|
162
|
+
return this.parentTargets.some((checkbox) => checkbox === node) || this.childTargets.some((checkbox) => checkbox === node);
|
|
52
163
|
}
|
|
53
164
|
/**
|
|
54
165
|
* Computes the aggregate state. With children it counts them; with none it
|
|
@@ -2,6 +2,13 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/clipboard_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/default_attribute.ts
|
|
6
|
+
function setDefaultAttribute(element, name, value) {
|
|
7
|
+
if (element.hasAttribute(name)) return false;
|
|
8
|
+
element.setAttribute(name, value);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
// src/utils/safe_timeout.ts
|
|
6
13
|
var TimerRegistry = class {
|
|
7
14
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -75,9 +82,7 @@ var ClipboardController = class extends Controller {
|
|
|
75
82
|
*/
|
|
76
83
|
#resetTimerId = null;
|
|
77
84
|
connect() {
|
|
78
|
-
|
|
79
|
-
this.element.setAttribute("data-state", "idle");
|
|
80
|
-
}
|
|
85
|
+
setDefaultAttribute(this.element, "data-state", "idle");
|
|
81
86
|
}
|
|
82
87
|
disconnect() {
|
|
83
88
|
this.#timers.clearAll();
|
|
@@ -297,7 +297,10 @@ var CollapsibleController = class extends Controller {
|
|
|
297
297
|
#applyContent(content, open, waitForCloseTransition) {
|
|
298
298
|
if (open) {
|
|
299
299
|
content.hidden = false;
|
|
300
|
-
content.style.setProperty(
|
|
300
|
+
content.style.setProperty(
|
|
301
|
+
"--stimeo--collapsible-content-height",
|
|
302
|
+
`${content.scrollHeight}px`
|
|
303
|
+
);
|
|
301
304
|
content.setAttribute("data-state", "open");
|
|
302
305
|
return;
|
|
303
306
|
}
|
|
@@ -59,7 +59,7 @@ var MicrotaskCoalescer = class {
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
// src/controllers/color_picker_controller.ts
|
|
62
|
-
var COLOR_PROPERTY = "--stimeo
|
|
62
|
+
var COLOR_PROPERTY = "--stimeo--color";
|
|
63
63
|
var CHANNEL_RANGE = {
|
|
64
64
|
hue: [0, 360],
|
|
65
65
|
saturation: [0, 100],
|
|
@@ -74,7 +74,7 @@ var ColorPickerController = class extends Controller {
|
|
|
74
74
|
logicalTrack: { type: Boolean, default: false }
|
|
75
75
|
};
|
|
76
76
|
static actions = ["onHexInput", "onKeydown", "onPointerDown"];
|
|
77
|
-
static events = ["change"];
|
|
77
|
+
static events = ["change", "reconcile"];
|
|
78
78
|
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
79
79
|
get #mirrored() {
|
|
80
80
|
return this.logicalTrackValue && isRtl(this.element);
|
|
@@ -83,14 +83,14 @@ var ColorPickerController = class extends Controller {
|
|
|
83
83
|
#color = { hue: 0, saturation: 0, lightness: 0, alpha: 100 };
|
|
84
84
|
/** Aborts in-progress pointer-drag listeners on drag end / teardown. */
|
|
85
85
|
#dragAbort = null;
|
|
86
|
-
/**
|
|
86
|
+
/** Color the last repaint settled on, so a configuration-driven move is reported once. */
|
|
87
|
+
#committedHex = null;
|
|
87
88
|
/**
|
|
88
89
|
* Collapses a morph that swaps render inputs into one repaint, and refuses the
|
|
89
90
|
* pass Stimulus delivers before `connect()`.
|
|
90
91
|
*/
|
|
91
|
-
#repaint = new MicrotaskCoalescer(() =>
|
|
92
|
-
|
|
93
|
-
});
|
|
92
|
+
#repaint = new MicrotaskCoalescer(() => this.#reconcileColor());
|
|
93
|
+
/** Seeds the model from the initial hex value and renders every surface. */
|
|
94
94
|
connect() {
|
|
95
95
|
this.#repaint.activate();
|
|
96
96
|
const parsed = hexToHsla(this.valueValue);
|
|
@@ -181,14 +181,30 @@ var ColorPickerController = class extends Controller {
|
|
|
181
181
|
return;
|
|
182
182
|
}
|
|
183
183
|
this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };
|
|
184
|
-
this.#
|
|
184
|
+
this.#commitColor();
|
|
185
185
|
}
|
|
186
186
|
/** Clamps and snaps one channel to an integer, then re-renders + emits change. */
|
|
187
187
|
#setChannel(channel, raw, min, max) {
|
|
188
188
|
this.#color[channel] = Math.round(Math.min(max, Math.max(min, raw)));
|
|
189
|
+
this.#commitColor();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Renders the model and reports a color the user actually moved. A key pressed
|
|
193
|
+
* at a bound, a pointer that lands on the step already showing, and a re-confirmed
|
|
194
|
+
* hex all leave the committed color where it was, so no `change` describes them.
|
|
195
|
+
*/
|
|
196
|
+
#commitColor() {
|
|
197
|
+
const previous = this.#committedHex;
|
|
189
198
|
this.#render();
|
|
199
|
+
if (this.#committedHex !== previous) {
|
|
200
|
+
this.dispatch("change", { detail: this.#settledDetail() });
|
|
201
|
+
}
|
|
190
202
|
}
|
|
191
|
-
/**
|
|
203
|
+
/**
|
|
204
|
+
* Reflects the model onto sliders, the hex input, preview, and form field.
|
|
205
|
+
*
|
|
206
|
+
* @stimeoRenderRoot
|
|
207
|
+
*/
|
|
192
208
|
#render() {
|
|
193
209
|
for (const slider of this.sliderTargets) {
|
|
194
210
|
const channel = this.#channelOf(slider);
|
|
@@ -198,14 +214,28 @@ var ColorPickerController = class extends Controller {
|
|
|
198
214
|
slider.setAttribute("aria-valuetext", valueText(channel, value));
|
|
199
215
|
}
|
|
200
216
|
const hex = this.#hexString();
|
|
217
|
+
this.#committedHex = hex;
|
|
201
218
|
if (this.hasHexTarget) this.hexTarget.value = hex;
|
|
202
219
|
for (const field of this.fieldTargets) field.value = hex;
|
|
203
220
|
for (const preview of this.previewTargets) preview.style.setProperty(COLOR_PROPERTY, hex);
|
|
204
221
|
this.element.style.setProperty(COLOR_PROPERTY, hex);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Repaints after `alpha` changed at runtime and reports a color this controller
|
|
225
|
+
* settled on. Disabling alpha drops it from the model, so the committed color can
|
|
226
|
+
* move without a user edit; `change` stays reserved for the picker's own actions.
|
|
227
|
+
*/
|
|
228
|
+
#reconcileColor() {
|
|
229
|
+
const previous = this.#committedHex;
|
|
230
|
+
this.#render();
|
|
231
|
+
if (previous !== null && this.#committedHex !== previous) {
|
|
232
|
+
this.dispatch("reconcile", { detail: this.#settledDetail() });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** The settled color as event detail, shared by both report paths. */
|
|
236
|
+
#settledDetail() {
|
|
205
237
|
const rgb = hslToRgb(this.#color.hue, this.#color.saturation, this.#color.lightness);
|
|
206
|
-
this
|
|
207
|
-
detail: { value: hex, rgba: { ...rgb, a: this.#color.alpha / 100 } }
|
|
208
|
-
});
|
|
238
|
+
return { value: this.#hexString(), rgba: { ...rgb, a: this.#color.alpha / 100 } };
|
|
209
239
|
}
|
|
210
240
|
/** The current color as `#RRGGBB`, or `#RRGGBBAA` when alpha is enabled. */
|
|
211
241
|
#hexString() {
|
|
@@ -237,8 +237,8 @@ var ContextMenuController = class extends Controller {
|
|
|
237
237
|
onDismiss: () => this.#closeAndRestore(),
|
|
238
238
|
claims: claimsWhileFocusWithin(this.element)
|
|
239
239
|
});
|
|
240
|
-
this.menuTarget.style.setProperty("--stimeo
|
|
241
|
-
this.menuTarget.style.setProperty("--stimeo
|
|
240
|
+
this.menuTarget.style.setProperty("--stimeo--context-menu-x", `${x}px`);
|
|
241
|
+
this.menuTarget.style.setProperty("--stimeo--context-menu-y", `${y}px`);
|
|
242
242
|
this.menuTarget.hidden = false;
|
|
243
243
|
if (this.hasRegionTarget) this.regionTarget.setAttribute("data-state", "open");
|
|
244
244
|
this.#navigableItems[0]?.focus();
|
|
@@ -268,7 +268,11 @@ var CountdownController = class extends Controller {
|
|
|
268
268
|
const raw = this.#isDown ? this.#reference - now : now - this.#reference;
|
|
269
269
|
return Math.max(0, raw);
|
|
270
270
|
}
|
|
271
|
-
/**
|
|
271
|
+
/**
|
|
272
|
+
* Writes the amount into the day/hour/minute/second slots.
|
|
273
|
+
*
|
|
274
|
+
* @stimeoRenderRoot
|
|
275
|
+
*/
|
|
272
276
|
#render(amount) {
|
|
273
277
|
const totalSeconds = Math.floor(amount / SECOND_MS);
|
|
274
278
|
this.#renderedAmount = totalSeconds * SECOND_MS;
|