stimeo-ui 0.5.0 → 0.7.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 +178 -0
- package/dist/controllers/alert_dialog_controller.d.ts +2 -0
- package/dist/controllers/alert_dialog_controller.js +75 -4
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.d.ts +1 -3
- package/dist/controllers/aspect_ratio_controller.js +19 -11
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +2 -0
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.d.ts +36 -15
- package/dist/controllers/avatar_controller.js +237 -40
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +2 -0
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +2 -0
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +2 -0
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +12 -3
- package/dist/controllers/carousel_controller.js +85 -9
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +52 -20
- package/dist/controllers/character_counter_controller.js +338 -63
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +34 -5
- package/dist/controllers/checkbox_controller.js +136 -25
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +10 -1
- package/dist/controllers/color_picker_controller.js +35 -9
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +2 -0
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +2 -0
- package/dist/controllers/command_palette_controller.js +75 -4
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +42 -14
- package/dist/controllers/conditional_fields_controller.js +345 -51
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +2 -0
- package/dist/controllers/confirm_controller.js +75 -4
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +2 -0
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +4 -0
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +2 -0
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +3 -0
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +23 -4
- package/dist/controllers/date_range_picker_controller.js +157 -30
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +75 -4
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +56 -24
- package/dist/controllers/direct_upload_controller.js +201 -45
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +14 -6
- package/dist/controllers/dirty_form_controller.js +192 -29
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +2 -0
- package/dist/controllers/dismissible_controller.js +83 -18
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js +75 -4
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +2 -0
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +9 -1
- package/dist/controllers/file_dropzone_controller.js +26 -3
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +2 -0
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +4 -0
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +4 -3
- package/dist/controllers/focus_controller.js +75 -4
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.d.ts +50 -10
- package/dist/controllers/form_field_controller.js +280 -62
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.d.ts +10 -8
- package/dist/controllers/form_validation_controller.js +208 -83
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +2 -0
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +2 -0
- 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 -3
- package/dist/controllers/idle_controller.js +27 -5
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +2 -0
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +2 -0
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +2 -0
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +2 -0
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +2 -0
- 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.d.ts +2 -0
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +48 -12
- package/dist/controllers/multi_select_controller.js +460 -151
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +2 -0
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +2 -0
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +26 -6
- package/dist/controllers/number_input_controller.js +317 -51
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +3 -0
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +2 -0
- package/dist/controllers/overflow_menu_controller.js +6 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +2 -0
- package/dist/controllers/pagination_controller.js +35 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +2 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +5 -3
- package/dist/controllers/password_strength_controller.js +20 -2
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +33 -19
- package/dist/controllers/persist_controller.js +449 -120
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +3 -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 +77 -4
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +3 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +2 -0
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +3 -0
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +44 -15
- package/dist/controllers/radio_group_controller.js +540 -56
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +38 -31
- package/dist/controllers/rating_controller.js +276 -89
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +2 -0
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +2 -0
- package/dist/controllers/resizable_controller.js +33 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +6 -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 +25 -11
- package/dist/controllers/scroll_area_controller.js +557 -125
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
- package/dist/controllers/scroll_visibility_controller.js +33 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +2 -0
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +54 -11
- package/dist/controllers/separator_controller.js +354 -38
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.js +83 -10
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -0
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +2 -0
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +2 -0
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +2 -0
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +2 -0
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +94 -38
- package/dist/controllers/submit_once_controller.js +399 -121
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +2 -0
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +43 -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.d.ts +2 -0
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +2 -0
- package/dist/controllers/theme_controller.js +8 -6
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +44 -13
- package/dist/controllers/time_picker_controller.js +296 -107
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +4 -0
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +41 -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 +4 -3
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +2 -0
- package/dist/controllers/tree_view_controller.js +7 -4
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +4963 -1581
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +123 -6
- package/dist/inspector/cli.js +212 -18
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +274 -50
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +15 -15
- package/dist/inspector/manifest.json +602 -80
- package/dist/positioning/index.d.ts +4 -2
- package/dist/positioning/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -21,28 +21,57 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
21
21
|
* `indeterminate` property (announced as `mixed`) derived from its children, and
|
|
22
22
|
* the cascade from a parent toggle to its children.
|
|
23
23
|
*
|
|
24
|
+
* `change` and `reconcile` dispatch
|
|
25
|
+
* `{ checked: boolean, indeterminate: boolean, state: "all" | "partial" | "none" }`.
|
|
26
|
+
*
|
|
24
27
|
* @remarks
|
|
25
28
|
* Behavior only — the check mark and any "mixed" affordance are the consumer's
|
|
26
|
-
* CSS (keyed off `:checked` / `:indeterminate` / `data-state`).
|
|
27
|
-
*
|
|
29
|
+
* CSS (keyed off `:checked` / `:indeterminate` / `data-state`). The `parent`
|
|
30
|
+
* target is optional: a child-only group still exposes its aggregate on the root,
|
|
31
|
+
* while a lone tri-state checkbox can use just `parent` and drive
|
|
32
|
+
* `indeterminate` externally — with no `child` target the parent's own `checked`
|
|
33
|
+
* and `indeterminate` stay the consumer's, because only children are
|
|
34
|
+
* authoritative over them. At most one `parent` may be present.
|
|
28
35
|
*
|
|
29
36
|
* Behavior provided:
|
|
30
37
|
* - Parent toggle checks/unchecks every child and clears its own `indeterminate`.
|
|
38
|
+
* All child targets, including disabled ones, participate; omit the target to
|
|
39
|
+
* exclude an input. The cascade does not synthesize native child `change`
|
|
40
|
+
* events — one aggregate custom event describes the action.
|
|
31
41
|
* - A child change recomputes the parent: all → checked, none → unchecked,
|
|
32
42
|
* some → `indeterminate`.
|
|
33
43
|
* - The aggregate (`all` / `partial` / `none`) is mirrored to `data-state` on the
|
|
34
|
-
* root,
|
|
44
|
+
* root. Dynamic targets, checked-attribute changes, Turbo morphs, and native
|
|
45
|
+
* form resets are reconciled from the children. Callers that assign the live
|
|
46
|
+
* `checked` property directly must dispatch `change`, because property writes
|
|
47
|
+
* are not observable by a `MutationObserver`.
|
|
48
|
+
* - `stimeo--checkbox:change` is dispatched for the two public change actions;
|
|
49
|
+
* `stimeo--checkbox:reconcile` is dispatched instead when a reconciliation —
|
|
50
|
+
* not the user — moves the aggregate. Both carry `{ checked: boolean,
|
|
51
|
+
* indeterminate: boolean, state: "all" | "partial" | "none" }` and describe
|
|
52
|
+
* the aggregate even without a parent. Neither fires on connect.
|
|
35
53
|
*/
|
|
36
54
|
declare class CheckboxController extends Controller<HTMLElement> {
|
|
37
55
|
#private;
|
|
38
56
|
static targets: string[];
|
|
39
57
|
static actions: readonly ["onChildChange", "onParentChange"];
|
|
40
|
-
static events: readonly ["change"];
|
|
58
|
+
static events: readonly ["change", "reconcile"];
|
|
41
59
|
readonly parentTarget: HTMLInputElement;
|
|
60
|
+
readonly parentTargets: HTMLInputElement[];
|
|
42
61
|
readonly hasParentTarget: boolean;
|
|
43
62
|
readonly childTargets: HTMLInputElement[];
|
|
44
|
-
/** Reflects the initial aggregate
|
|
63
|
+
/** Reflects the initial aggregate and starts retained-DOM reconciliation. */
|
|
45
64
|
connect(): void;
|
|
65
|
+
/** Releases the observer, global reset listener, and every pending reconciliation. */
|
|
66
|
+
disconnect(): void;
|
|
67
|
+
/** Reconciles the aggregate for a parent added or replaced at runtime. */
|
|
68
|
+
parentTargetConnected(): void;
|
|
69
|
+
/** Reconciles the aggregate after a parent target leaves the group. */
|
|
70
|
+
parentTargetDisconnected(): void;
|
|
71
|
+
/** Reconciles the aggregate for a child added at runtime. */
|
|
72
|
+
childTargetConnected(): void;
|
|
73
|
+
/** Reconciles the aggregate after a child leaves the group. */
|
|
74
|
+
childTargetDisconnected(): void;
|
|
46
75
|
/** Cascades the parent's state to every child. Bound via `data-action` (change). */
|
|
47
76
|
onParentChange(): void;
|
|
48
77
|
/** Recomputes the parent from its children. Bound via `data-action` (change). */
|
|
@@ -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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/checkbox_controller.ts"],"names":[],"mappings":"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","file":"checkbox_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** Aggregate selection state of a parent/child checkbox group. */\ntype CheckboxState = \"all\" | \"partial\" | \"none\";\n\n/**\n * Headless, accessible checkbox behavior (including the tri-state / parent-child\n * \"select all\" pattern).\n *\n * Markup contract (identifier: `stimeo--checkbox`):\n * <fieldset data-controller=\"stimeo--checkbox\" role=\"group\" aria-labelledby=\"all-label\">\n * <label id=\"all-label\">\n * <input type=\"checkbox\" data-stimeo--checkbox-target=\"parent\"\n * data-action=\"change->stimeo--checkbox#onParentChange\" /> Select all\n * </label>\n * <label><input type=\"checkbox\" data-stimeo--checkbox-target=\"child\"\n * data-action=\"change->stimeo--checkbox#onChildChange\" /> Item A</label>\n * <!-- more children -->\n * </fieldset>\n *\n * Implements the WAI-ARIA APG **Checkbox** pattern. Native `<input type=\"checkbox\">`\n * is used throughout (so Space toggle, focus order, and `mixed` exposure are the\n * browser's); the controller adds what HTML cannot express: the parent's\n * `indeterminate` property (announced as `mixed`) derived from its children, and\n * the cascade from a parent toggle to its children.\n *\n * @remarks\n * Behavior only — the check mark and any \"mixed\" affordance are the consumer's\n * CSS (keyed off `:checked` / `:indeterminate` / `data-state`). A lone tri-state\n * checkbox can use just `parent` and drive `indeterminate` externally.\n *\n * Behavior provided:\n * - Parent toggle checks/unchecks every child and clears its own `indeterminate`.\n * - A child change recomputes the parent: all → checked, none → unchecked,\n * some → `indeterminate`.\n * - The aggregate (`all` / `partial` / `none`) is mirrored to `data-state` on the\n * root, and `stimeo--checkbox:change` is dispatched on every change.\n */\nexport class CheckboxController extends Controller<HTMLElement> {\n static override targets = [\"parent\", \"child\"];\n static actions = [\"onChildChange\", \"onParentChange\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly parentTarget: HTMLInputElement;\n declare readonly hasParentTarget: boolean;\n declare readonly childTargets: HTMLInputElement[];\n\n /** Reflects the initial aggregate (e.g. from server-rendered child states). */\n override connect(): void {\n if (this.childTargets.length > 0) {\n this.#syncFromChildren(false);\n } else {\n this.element.setAttribute(\"data-state\", this.#aggregate());\n }\n }\n\n /** Cascades the parent's state to every child. Bound via `data-action` (change). */\n onParentChange(): void {\n if (!this.hasParentTarget) return;\n const checked = this.parentTarget.checked;\n for (const child of this.childTargets) {\n child.checked = checked;\n }\n this.parentTarget.indeterminate = false;\n const state: CheckboxState = checked ? \"all\" : \"none\";\n this.element.setAttribute(\"data-state\", state);\n this.dispatch(\"change\", { detail: { checked, indeterminate: false, state } });\n }\n\n /** Recomputes the parent from its children. Bound via `data-action` (change). */\n onChildChange(): void {\n this.#syncFromChildren(true);\n }\n\n /**\n * Derives the parent's `checked`/`indeterminate` and the root `data-state` from\n * the children, optionally dispatching `change`.\n */\n #syncFromChildren(dispatch: boolean): void {\n const state = this.#aggregate();\n if (this.hasParentTarget) {\n this.parentTarget.checked = state === \"all\";\n this.parentTarget.indeterminate = state === \"partial\";\n }\n this.element.setAttribute(\"data-state\", state);\n if (dispatch) {\n this.dispatch(\"change\", {\n detail: {\n checked: this.hasParentTarget ? this.parentTarget.checked : state === \"all\",\n indeterminate: this.hasParentTarget ? this.parentTarget.indeterminate : false,\n state,\n },\n });\n }\n }\n\n /**\n * Computes the aggregate state. With children it counts them; with none it\n * reads the parent so a lone tri-state checkbox still reports a state without\n * its externally-set `indeterminate` being clobbered.\n */\n #aggregate(): CheckboxState {\n const children = this.childTargets;\n if (children.length === 0) {\n if (this.hasParentTarget && this.parentTarget.indeterminate) return \"partial\";\n return this.hasParentTarget && this.parentTarget.checked ? \"all\" : \"none\";\n }\n const checked = children.filter((child) => child.checked).length;\n if (checked === 0) return \"none\";\n if (checked === children.length) return \"all\";\n return \"partial\";\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/microtask_coalescer.ts","../../src/controllers/checkbox_controller.ts"],"names":[],"mappings":"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","file":"checkbox_controller.js","sourcesContent":["/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/** Aggregate selection state of a parent/child checkbox group. */\ntype CheckboxState = \"all\" | \"partial\" | \"none\";\n\n/**\n * Headless, accessible checkbox behavior (including the tri-state / parent-child\n * \"select all\" pattern).\n *\n * Markup contract (identifier: `stimeo--checkbox`):\n * <fieldset data-controller=\"stimeo--checkbox\" role=\"group\" aria-labelledby=\"all-label\">\n * <label id=\"all-label\">\n * <input type=\"checkbox\" data-stimeo--checkbox-target=\"parent\"\n * data-action=\"change->stimeo--checkbox#onParentChange\" /> Select all\n * </label>\n * <label><input type=\"checkbox\" data-stimeo--checkbox-target=\"child\"\n * data-action=\"change->stimeo--checkbox#onChildChange\" /> Item A</label>\n * <!-- more children -->\n * </fieldset>\n *\n * Implements the WAI-ARIA APG **Checkbox** pattern. Native `<input type=\"checkbox\">`\n * is used throughout (so Space toggle, focus order, and `mixed` exposure are the\n * browser's); the controller adds what HTML cannot express: the parent's\n * `indeterminate` property (announced as `mixed`) derived from its children, and\n * the cascade from a parent toggle to its children.\n *\n * `change` and `reconcile` dispatch\n * `{ checked: boolean, indeterminate: boolean, state: \"all\" | \"partial\" | \"none\" }`.\n *\n * @remarks\n * Behavior only — the check mark and any \"mixed\" affordance are the consumer's\n * CSS (keyed off `:checked` / `:indeterminate` / `data-state`). The `parent`\n * target is optional: a child-only group still exposes its aggregate on the root,\n * while a lone tri-state checkbox can use just `parent` and drive\n * `indeterminate` externally — with no `child` target the parent's own `checked`\n * and `indeterminate` stay the consumer's, because only children are\n * authoritative over them. At most one `parent` may be present.\n *\n * Behavior provided:\n * - Parent toggle checks/unchecks every child and clears its own `indeterminate`.\n * All child targets, including disabled ones, participate; omit the target to\n * exclude an input. The cascade does not synthesize native child `change`\n * events — one aggregate custom event describes the action.\n * - A child change recomputes the parent: all → checked, none → unchecked,\n * some → `indeterminate`.\n * - The aggregate (`all` / `partial` / `none`) is mirrored to `data-state` on the\n * root. Dynamic targets, checked-attribute changes, Turbo morphs, and native\n * form resets are reconciled from the children. Callers that assign the live\n * `checked` property directly must dispatch `change`, because property writes\n * are not observable by a `MutationObserver`.\n * - `stimeo--checkbox:change` is dispatched for the two public change actions;\n * `stimeo--checkbox:reconcile` is dispatched instead when a reconciliation —\n * not the user — moves the aggregate. Both carry `{ checked: boolean,\n * indeterminate: boolean, state: \"all\" | \"partial\" | \"none\" }` and describe\n * the aggregate even without a parent. Neither fires on connect.\n */\nexport class CheckboxController extends Controller<HTMLElement> {\n static override targets = [\"parent\", \"child\"];\n static actions = [\"onChildChange\", \"onParentChange\"] as const;\n static events = [\"change\", \"reconcile\"] as const;\n\n declare readonly parentTarget: HTMLInputElement;\n declare readonly parentTargets: HTMLInputElement[];\n declare readonly hasParentTarget: boolean;\n declare readonly childTargets: HTMLInputElement[];\n\n /** Collapses every lifecycle signal from one DOM update into one derived pass. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileFromChildren());\n /** Aggregate this root last settled on, so a derived repair is reported once. */\n #committedState: CheckboxState | null = null;\n /** Watches authored checked-attribute changes on retained target elements. */\n readonly #checkedObserver = new MutationObserver((records) => {\n if (records.some((record) => this.#isManagedCheckbox(record.target))) {\n this.#reconcile.schedule();\n }\n });\n\n /** Reflects the initial aggregate and starts retained-DOM reconciliation. */\n override connect(): void {\n this.#reconcile.activate();\n this.#syncFromChildren();\n this.#checkedObserver.observe(this.element, {\n attributes: true,\n attributeFilter: [\"checked\"],\n subtree: true,\n });\n this.element.addEventListener(\"turbo:morph-element\", this.#onMorph);\n document.addEventListener(\"reset\", this.#onReset, true);\n }\n\n /** Releases the observer, global reset listener, and every pending reconciliation. */\n override disconnect(): void {\n this.#reconcile.cancel();\n this.#checkedObserver.disconnect();\n this.element.removeEventListener(\"turbo:morph-element\", this.#onMorph);\n document.removeEventListener(\"reset\", this.#onReset, true);\n }\n\n /** Reconciles the aggregate for a parent added or replaced at runtime. */\n parentTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Reconciles the aggregate after a parent target leaves the group. */\n parentTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Reconciles the aggregate for a child added at runtime. */\n childTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Reconciles the aggregate after a child leaves the group. */\n childTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Cascades the parent's state to every child. Bound via `data-action` (change). */\n onParentChange(): void {\n if (!this.hasParentTarget) return;\n const checked = this.parentTarget.checked;\n for (const child of this.childTargets) {\n child.checked = checked;\n }\n this.#reflect(checked ? \"all\" : \"none\", true);\n const detail = this.#settledDetail();\n if (detail) this.dispatch(\"change\", { detail });\n }\n\n /** Recomputes the parent from its children. Bound via `data-action` (change). */\n onChildChange(): void {\n this.#syncFromChildren();\n const detail = this.#settledDetail();\n if (detail) this.dispatch(\"change\", { detail });\n }\n\n /**\n * Announces an aggregate this pass derived rather than the user. `change` stays\n * reserved for the two public actions, so automation never reads a repair as an edit.\n */\n #reconcileFromChildren(): void {\n const previous = this.#committedState;\n this.#syncFromChildren();\n if (this.#committedState === previous) return;\n const detail = this.#settledDetail();\n if (detail) this.dispatch(\"reconcile\", { detail });\n }\n\n /**\n * Derives the parent's `checked`/`indeterminate` and the root `data-state` from\n * the children. Writing state and reporting it are separate so the caller — not\n * a flag threaded through the write — decides which event describes the cause.\n */\n #syncFromChildren(): void {\n // A childless root reads its aggregate off the parent itself, so writing the\n // derived value back would clear a `checked` the consumer drives alongside\n // `indeterminate`. Only the children are authoritative over the parent.\n this.#reflect(this.#aggregate(), this.childTargets.length > 0);\n }\n\n /**\n * Reflects one aggregate state.\n *\n * @param writeParent - whether the state is authoritative over the `parent`\n * target's own `checked` / `indeterminate`.\n */\n #reflect(state: CheckboxState, writeParent: boolean): void {\n if (writeParent && this.hasParentTarget) {\n this.parentTarget.checked = state === \"all\";\n this.parentTarget.indeterminate = state === \"partial\";\n }\n this.element.setAttribute(\"data-state\", state);\n this.#committedState = state;\n }\n\n /** The settled aggregate as event detail, or `null` before anything has settled. */\n #settledDetail(): { checked: boolean; indeterminate: boolean; state: CheckboxState } | null {\n const state = this.#committedState;\n if (state === null) return null;\n return { checked: state === \"all\", indeterminate: state === \"partial\", state };\n }\n\n /** Reconciles retained targets after Turbo has finished morphing their live state. */\n readonly #onMorph = (): void => {\n this.#reconcile.schedule();\n };\n\n /** Reconciles after a non-cancelled reset restores any managed checkbox. */\n readonly #onReset = (event: Event): void => {\n const form = event.target;\n if (!(form instanceof HTMLFormElement) || !this.#hasCheckboxOwnedBy(form)) return;\n queueMicrotask(() => {\n if (!event.defaultPrevented) this.#reconcile.schedule();\n });\n };\n\n /** Whether a form owns at least one current parent or child target. */\n #hasCheckboxOwnedBy(form: HTMLFormElement): boolean {\n return [...this.parentTargets, ...this.childTargets].some((checkbox) => checkbox.form === form);\n }\n\n /** Whether an observed attribute mutation belongs to this controller's target set. */\n #isManagedCheckbox(node: Node): boolean {\n return (\n this.parentTargets.some((checkbox) => checkbox === node) ||\n this.childTargets.some((checkbox) => checkbox === node)\n );\n }\n\n /**\n * Computes the aggregate state. With children it counts them; with none it\n * reads the parent so a lone tri-state checkbox still reports a state without\n * its externally-set `indeterminate` being clobbered.\n */\n #aggregate(): CheckboxState {\n const children = this.childTargets;\n if (children.length === 0) {\n if (this.hasParentTarget && this.parentTarget.indeterminate) return \"partial\";\n return this.hasParentTarget && this.parentTarget.checked ? \"all\" : \"none\";\n }\n const checked = children.filter((child) => child.checked).length;\n if (checked === 0) return \"none\";\n if (checked === children.length) return \"all\";\n return \"partial\";\n }\n}\n"]}
|
|
@@ -27,6 +27,9 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
27
27
|
* preview and root as the `--stimeo--color` custom property and mirrored into a
|
|
28
28
|
* hidden form field.
|
|
29
29
|
*
|
|
30
|
+
* `change` and `reconcile` dispatch
|
|
31
|
+
* `{ value: string, rgba: { r: number, g: number, b: number, a: number } }`.
|
|
32
|
+
*
|
|
30
33
|
* @remarks
|
|
31
34
|
* Behavior only — the swatch/gradient visuals are the consumer's CSS/canvas, fed
|
|
32
35
|
* by `--stimeo--color`. Only the consumer knows whether a channel track mirrors
|
|
@@ -44,6 +47,11 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
44
47
|
* exactly bijective: a typed hex can normalize to a near (not identical) value
|
|
45
48
|
* once the HSL sliders are touched. This keeps the model small and zero-dep; use a
|
|
46
49
|
* dedicated color library on the consumer side if exact hex preservation matters.
|
|
50
|
+
*
|
|
51
|
+
* A color the user set through a slider or the hex input is reported as
|
|
52
|
+
* `stimeo--color-picker:change`. Toggling `alpha` at runtime can move the
|
|
53
|
+
* committed color without a user edit, and that arrives as
|
|
54
|
+
* `stimeo--color-picker:reconcile` with the same detail. Neither fires on connect.
|
|
47
55
|
*/
|
|
48
56
|
declare class ColorPickerController extends Controller<HTMLElement> {
|
|
49
57
|
#private;
|
|
@@ -63,7 +71,7 @@ declare class ColorPickerController extends Controller<HTMLElement> {
|
|
|
63
71
|
};
|
|
64
72
|
};
|
|
65
73
|
static actions: readonly ["onHexInput", "onKeydown", "onPointerDown"];
|
|
66
|
-
static events: readonly ["change"];
|
|
74
|
+
static events: readonly ["change", "reconcile"];
|
|
67
75
|
readonly sliderTargets: HTMLElement[];
|
|
68
76
|
readonly hexTarget: HTMLInputElement;
|
|
69
77
|
readonly hasHexTarget: boolean;
|
|
@@ -72,6 +80,7 @@ declare class ColorPickerController extends Controller<HTMLElement> {
|
|
|
72
80
|
valueValue: string;
|
|
73
81
|
alphaValue: boolean;
|
|
74
82
|
logicalTrackValue: boolean;
|
|
83
|
+
/** Seeds the model from the initial hex value and renders every surface. */
|
|
75
84
|
connect(): void;
|
|
76
85
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
77
86
|
disconnect(): void;
|
|
@@ -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,12 +181,24 @@ 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
|
/**
|
|
192
204
|
* Reflects the model onto sliders, the hex input, preview, and form field.
|
|
@@ -202,14 +214,28 @@ var ColorPickerController = class extends Controller {
|
|
|
202
214
|
slider.setAttribute("aria-valuetext", valueText(channel, value));
|
|
203
215
|
}
|
|
204
216
|
const hex = this.#hexString();
|
|
217
|
+
this.#committedHex = hex;
|
|
205
218
|
if (this.hasHexTarget) this.hexTarget.value = hex;
|
|
206
219
|
for (const field of this.fieldTargets) field.value = hex;
|
|
207
220
|
for (const preview of this.previewTargets) preview.style.setProperty(COLOR_PROPERTY, hex);
|
|
208
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() {
|
|
209
237
|
const rgb = hslToRgb(this.#color.hue, this.#color.saturation, this.#color.lightness);
|
|
210
|
-
this
|
|
211
|
-
detail: { value: hex, rgba: { ...rgb, a: this.#color.alpha / 100 } }
|
|
212
|
-
});
|
|
238
|
+
return { value: this.#hexString(), rgba: { ...rgb, a: this.#color.alpha / 100 } };
|
|
213
239
|
}
|
|
214
240
|
/** The current color as `#RRGGBB`, or `#RRGGBBAA` when alpha is enabled. */
|
|
215
241
|
#hexString() {
|