stimeo-ui 0.6.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 +62 -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/auto_submit_controller.d.ts +2 -0
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +47 -5
- 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/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 +3 -0
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +3 -0
- 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/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 +2 -0
- 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 +2 -0
- 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/idle_controller.d.ts +2 -0
- 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/meter_controller.d.ts +2 -0
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +2 -0
- 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 +2 -0
- package/dist/controllers/number_input_controller.js +47 -5
- 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 +2 -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 +2 -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 +2 -0
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +30 -16
- package/dist/controllers/persist_controller.js +432 -122
- 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.js +77 -4
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +2 -0
- 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 +3 -0
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +2 -0
- package/dist/controllers/rating_controller.js +9 -5
- 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.map +1 -1
- package/dist/controllers/roving_controller.d.ts +2 -0
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +23 -15
- package/dist/controllers/scroll_area_controller.js +462 -162
- 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.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 +2 -0
- 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 +2 -0
- 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 +2 -0
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +2 -0
- 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 +2 -1
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +2387 -861
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +107 -3
- package/dist/inspector/cli.js +192 -14
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +254 -46
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +7 -7
- package/dist/inspector/manifest.json +434 -44
- package/dist/positioning/index.d.ts +2 -0
- package/dist/positioning/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
/** Trigger controls whose state drives region visibility. */
|
|
3
|
+
/** Trigger controls whose live state drives region visibility. */
|
|
4
4
|
type Trigger = HTMLInputElement | HTMLSelectElement;
|
|
5
5
|
/**
|
|
6
6
|
* Headless dependent-fields behavior: shows/hides (and enables/disables) regions
|
|
7
|
-
*
|
|
7
|
+
* from trigger control values while keeping submission and accessibility state in
|
|
8
8
|
* sync (no dedicated APG pattern).
|
|
9
9
|
*
|
|
10
10
|
* Markup contract (identifier: `stimeo--conditional-fields`):
|
|
@@ -15,17 +15,32 @@ type Trigger = HTMLInputElement | HTMLSelectElement;
|
|
|
15
15
|
* </fieldset>
|
|
16
16
|
* </form>
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* A region declares one condition: `data-when-checked`, `data-when-unchecked`,
|
|
19
|
+
* or `data-when-value="x"`. The condition is combined across all triggers using
|
|
20
|
+
* `match` (`"any"` by default, or `"all"`). A region with no condition preserves
|
|
21
|
+
* its authored `hidden` state; a declared condition with no triggers is false.
|
|
22
|
+
*
|
|
23
|
+
* Hidden regions receive `hidden` and `aria-hidden="true"`; visible regions expose
|
|
24
|
+
* `data-visible="true"`. With `disableHidden`, descendant controls are disabled and
|
|
25
|
+
* marked with `data-conditional-disabled="true"`, and only marked controls are
|
|
26
|
+
* re-enabled. Before a focused region is hidden, focus moves to the first trigger
|
|
27
|
+
* that can actually receive it. If every trigger is unsafe — including one inside
|
|
28
|
+
* another region being hidden in the same pass — the controller root temporarily
|
|
29
|
+
* receives `tabindex="-1"` and focus.
|
|
30
|
+
*
|
|
31
|
+
* `change` and `reconcile` dispatch `{ region, visible }`.
|
|
24
32
|
*
|
|
25
33
|
* @remarks
|
|
26
|
-
* Behavior only.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
34
|
+
* Behavior only. Trigger `input` / `change`, dynamic targets and descendants,
|
|
35
|
+
* condition attributes, Values, native form reset, and Turbo retained-element
|
|
36
|
+
* morphs all reconcile against the live DOM. Direct writes to an input's live
|
|
37
|
+
* `checked` / `value` property remain observable only when the caller also emits
|
|
38
|
+
* the corresponding native event. DOM repair is silent. User input or the public
|
|
39
|
+
* action dispatches `stimeo--conditional-fields:change`; a lifecycle, Value,
|
|
40
|
+
* reset, mutation, or morph reconciliation dispatches
|
|
41
|
+
* `stimeo--conditional-fields:reconcile` instead. Initial connection is silent.
|
|
42
|
+
* Every listener, observer, queued pass, and borrowed root `tabindex` is released
|
|
43
|
+
* on `disconnect()`.
|
|
29
44
|
*/
|
|
30
45
|
declare class ConditionalFieldsController extends Controller<HTMLElement> {
|
|
31
46
|
#private;
|
|
@@ -41,15 +56,28 @@ declare class ConditionalFieldsController extends Controller<HTMLElement> {
|
|
|
41
56
|
};
|
|
42
57
|
};
|
|
43
58
|
static actions: readonly ["evaluate"];
|
|
44
|
-
static events: readonly ["change"];
|
|
59
|
+
static events: readonly ["change", "reconcile"];
|
|
45
60
|
readonly triggerTargets: Trigger[];
|
|
46
61
|
readonly regionTargets: HTMLElement[];
|
|
47
|
-
readonly hasTriggerTarget: boolean;
|
|
48
62
|
disableHiddenValue: boolean;
|
|
49
63
|
matchValue: string;
|
|
64
|
+
/** Reflects initial live state and opens every retained-DOM reconciliation path. */
|
|
50
65
|
connect(): void;
|
|
66
|
+
/** Releases all external resources and returns the temporary focus landmark loan. */
|
|
51
67
|
disconnect(): void;
|
|
52
|
-
/**
|
|
68
|
+
/** Reconciles after a trigger target is inserted or restored. */
|
|
69
|
+
triggerTargetConnected(): void;
|
|
70
|
+
/** Reconciles after a trigger target is removed or replaced. */
|
|
71
|
+
triggerTargetDisconnected(): void;
|
|
72
|
+
/** Initializes a region target inserted or restored at runtime. */
|
|
73
|
+
regionTargetConnected(): void;
|
|
74
|
+
/** Reconciles the remaining region set after a target leaves. */
|
|
75
|
+
regionTargetDisconnected(): void;
|
|
76
|
+
/** Reconciles when application code or a morph changes the match policy. */
|
|
77
|
+
matchValueChanged(): void;
|
|
78
|
+
/** Reconciles control ownership when hidden controls become enabled or excluded. */
|
|
79
|
+
disableHiddenValueChanged(): void;
|
|
80
|
+
/** Re-evaluates every region and repairs all reflected DOM state. */
|
|
53
81
|
evaluate(): void;
|
|
54
82
|
}
|
|
55
83
|
|
|
@@ -1,7 +1,134 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
// src/controllers/conditional_fields_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/focus_candidate.ts
|
|
6
|
+
function inheritsFieldsetDisabled(control) {
|
|
7
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
8
|
+
while (fieldset) {
|
|
9
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
10
|
+
if (!legend?.contains(control)) return true;
|
|
11
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function canTakeFocus(element) {
|
|
16
|
+
if (element.closest("[hidden], [inert]")) return false;
|
|
17
|
+
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
18
|
+
if (!("disabled" in element)) return true;
|
|
19
|
+
if (element.disabled) return false;
|
|
20
|
+
return !inheritsFieldsetDisabled(element);
|
|
21
|
+
}
|
|
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
|
+
|
|
56
|
+
// src/utils/before_cache_reset.ts
|
|
57
|
+
var BeforeCacheReset = class _BeforeCacheReset {
|
|
58
|
+
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
59
|
+
static #subscribers = /* @__PURE__ */ new Set();
|
|
60
|
+
/** The shared listener; installed while at least one instance is subscribed. */
|
|
61
|
+
static #onBeforeCache = () => {
|
|
62
|
+
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
63
|
+
};
|
|
64
|
+
#rewind;
|
|
65
|
+
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
66
|
+
constructor(rewind) {
|
|
67
|
+
this.#rewind = rewind;
|
|
68
|
+
}
|
|
69
|
+
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
70
|
+
activate() {
|
|
71
|
+
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
72
|
+
_BeforeCacheReset.#subscribers.add(this);
|
|
73
|
+
if (first) {
|
|
74
|
+
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
78
|
+
deactivate() {
|
|
79
|
+
_BeforeCacheReset.#subscribers.delete(this);
|
|
80
|
+
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
81
|
+
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/utils/tabindex_loan.ts
|
|
86
|
+
var TabindexLoan = class {
|
|
87
|
+
#value;
|
|
88
|
+
#lent = /* @__PURE__ */ new Set();
|
|
89
|
+
/** Returns live loans before Turbo can copy them into its page snapshot. */
|
|
90
|
+
#beforeCache = new BeforeCacheReset(() => this.returnAll());
|
|
91
|
+
/**
|
|
92
|
+
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
93
|
+
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
94
|
+
* which a scroll region with no focusable content of its own needs.
|
|
95
|
+
*/
|
|
96
|
+
constructor(value = "-1") {
|
|
97
|
+
this.#value = value;
|
|
98
|
+
}
|
|
99
|
+
/** Lends `element` the value; no-ops when it already carries a `tabindex`. */
|
|
100
|
+
lend(element) {
|
|
101
|
+
if (element.hasAttribute("tabindex")) return;
|
|
102
|
+
element.setAttribute("tabindex", this.#value);
|
|
103
|
+
this.#lent.add(element);
|
|
104
|
+
this.#beforeCache.activate();
|
|
105
|
+
}
|
|
106
|
+
/** Takes back every loan whose value is still the one that was lent. */
|
|
107
|
+
returnAll() {
|
|
108
|
+
for (const element of this.#lent) {
|
|
109
|
+
if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
|
|
110
|
+
}
|
|
111
|
+
this.#lent.clear();
|
|
112
|
+
this.#beforeCache.deactivate();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
3
116
|
// src/controllers/conditional_fields_controller.ts
|
|
4
117
|
var DISABLED_MARKER = "data-conditional-disabled";
|
|
118
|
+
var OBSERVED_ATTRIBUTES = [
|
|
119
|
+
"hidden",
|
|
120
|
+
"aria-hidden",
|
|
121
|
+
"data-visible",
|
|
122
|
+
DISABLED_MARKER,
|
|
123
|
+
"disabled",
|
|
124
|
+
"data-when-checked",
|
|
125
|
+
"data-when-unchecked",
|
|
126
|
+
"data-when-value",
|
|
127
|
+
"checked",
|
|
128
|
+
"selected",
|
|
129
|
+
"value",
|
|
130
|
+
"type"
|
|
131
|
+
];
|
|
5
132
|
var ConditionalFieldsController = class extends Controller {
|
|
6
133
|
static targets = ["trigger", "region"];
|
|
7
134
|
static values = {
|
|
@@ -9,56 +136,171 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
9
136
|
match: { type: String, default: "any" }
|
|
10
137
|
};
|
|
11
138
|
static actions = ["evaluate"];
|
|
12
|
-
static events = ["change"];
|
|
139
|
+
static events = ["change", "reconcile"];
|
|
140
|
+
/** Logical state last committed for each current region; rebuilt on every connect. */
|
|
13
141
|
#lastVisible = /* @__PURE__ */ new WeakMap();
|
|
14
|
-
|
|
15
|
-
|
|
142
|
+
/** Controller writes awaiting an observer callback, separated from authored morphs. */
|
|
143
|
+
#internalWrites = /* @__PURE__ */ new WeakMap();
|
|
144
|
+
#observing = false;
|
|
145
|
+
/** Coalesces one target/morph/mutation batch into one full DOM reconciliation. */
|
|
146
|
+
#reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
|
|
147
|
+
/** Provides the last-resort focus landmark without claiming an authored tabindex. */
|
|
148
|
+
#rootTabindex = new TabindexLoan();
|
|
149
|
+
/** Watches retained targets and controls whose declarative or reflected state changed. */
|
|
150
|
+
#observer = new MutationObserver((records) => this.#onMutations(records));
|
|
151
|
+
/** Evaluates only events whose source is one of this instance's live trigger targets. */
|
|
152
|
+
#onTriggerInput = (event) => {
|
|
153
|
+
const target = event.target;
|
|
154
|
+
const triggers = this.triggerTargets;
|
|
155
|
+
if (target instanceof Element && triggers.some((trigger) => trigger === target)) {
|
|
156
|
+
this.#commitChange(triggers);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
/** Reconciles property-only retained-element changes after Turbo morphs. */
|
|
160
|
+
#onMorph = () => {
|
|
161
|
+
this.#reconcile.schedule();
|
|
162
|
+
};
|
|
163
|
+
/** Reconciles the settled defaults after a non-cancelled owning form reset. */
|
|
164
|
+
#onReset = (event) => {
|
|
165
|
+
const form = event.target;
|
|
166
|
+
if (form instanceof HTMLFormElement && this.#hasTriggerOwnedBy(form)) {
|
|
167
|
+
queueMicrotask(() => {
|
|
168
|
+
if (!event.defaultPrevented) this.#reconcile.schedule();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
16
171
|
};
|
|
172
|
+
/** Reflects initial live state and opens every retained-DOM reconciliation path. */
|
|
17
173
|
connect() {
|
|
18
|
-
this
|
|
19
|
-
this
|
|
20
|
-
this.
|
|
174
|
+
this.#lastVisible = /* @__PURE__ */ new WeakMap();
|
|
175
|
+
this.#internalWrites = /* @__PURE__ */ new WeakMap();
|
|
176
|
+
this.#reconcile.activate();
|
|
177
|
+
this.#settle();
|
|
178
|
+
this.#observer.observe(this.element, {
|
|
179
|
+
attributes: true,
|
|
180
|
+
attributeFilter: OBSERVED_ATTRIBUTES,
|
|
181
|
+
childList: true,
|
|
182
|
+
subtree: true
|
|
183
|
+
});
|
|
184
|
+
this.#observing = true;
|
|
185
|
+
this.element.addEventListener("change", this.#onTriggerInput);
|
|
186
|
+
this.element.addEventListener("input", this.#onTriggerInput);
|
|
187
|
+
this.element.addEventListener("turbo:morph-element", this.#onMorph);
|
|
188
|
+
document.addEventListener("reset", this.#onReset, true);
|
|
21
189
|
}
|
|
190
|
+
/** Releases all external resources and returns the temporary focus landmark loan. */
|
|
22
191
|
disconnect() {
|
|
23
|
-
this.
|
|
24
|
-
this
|
|
192
|
+
this.#reconcile.cancel();
|
|
193
|
+
this.#observing = false;
|
|
194
|
+
this.#observer.disconnect();
|
|
195
|
+
this.element.removeEventListener("change", this.#onTriggerInput);
|
|
196
|
+
this.element.removeEventListener("input", this.#onTriggerInput);
|
|
197
|
+
this.element.removeEventListener("turbo:morph-element", this.#onMorph);
|
|
198
|
+
document.removeEventListener("reset", this.#onReset, true);
|
|
199
|
+
this.#rootTabindex.returnAll();
|
|
200
|
+
}
|
|
201
|
+
/** Reconciles after a trigger target is inserted or restored. */
|
|
202
|
+
triggerTargetConnected() {
|
|
203
|
+
this.#reconcile.schedule();
|
|
204
|
+
}
|
|
205
|
+
/** Reconciles after a trigger target is removed or replaced. */
|
|
206
|
+
triggerTargetDisconnected() {
|
|
207
|
+
this.#reconcile.schedule();
|
|
208
|
+
}
|
|
209
|
+
/** Initializes a region target inserted or restored at runtime. */
|
|
210
|
+
regionTargetConnected() {
|
|
211
|
+
this.#reconcile.schedule();
|
|
212
|
+
}
|
|
213
|
+
/** Reconciles the remaining region set after a target leaves. */
|
|
214
|
+
regionTargetDisconnected() {
|
|
215
|
+
this.#reconcile.schedule();
|
|
25
216
|
}
|
|
26
|
-
/**
|
|
217
|
+
/** Reconciles when application code or a morph changes the match policy. */
|
|
218
|
+
matchValueChanged() {
|
|
219
|
+
this.#reconcile.schedule();
|
|
220
|
+
}
|
|
221
|
+
/** Reconciles control ownership when hidden controls become enabled or excluded. */
|
|
222
|
+
disableHiddenValueChanged() {
|
|
223
|
+
this.#reconcile.schedule();
|
|
224
|
+
}
|
|
225
|
+
/** Re-evaluates every region and repairs all reflected DOM state. */
|
|
27
226
|
evaluate() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
227
|
+
this.#commitChange();
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Computes one settled plan, shows destinations before hiding sources, and then
|
|
231
|
+
* returns logical transitions in DOM order after every region is internally coherent.
|
|
232
|
+
*/
|
|
233
|
+
#settle(triggers = this.triggerTargets) {
|
|
234
|
+
const regions = this.regionTargets;
|
|
235
|
+
const plan = /* @__PURE__ */ new Map();
|
|
236
|
+
for (const region of regions) plan.set(region, this.#isVisible(region, triggers));
|
|
237
|
+
const changed = /* @__PURE__ */ new Set();
|
|
238
|
+
const applicationOrder = [
|
|
239
|
+
...regions.filter((region) => plan.get(region) === true),
|
|
240
|
+
...regions.filter((region) => plan.get(region) === false)
|
|
241
|
+
];
|
|
242
|
+
for (const region of applicationOrder) {
|
|
243
|
+
const visible = plan.get(region);
|
|
244
|
+
if (visible !== void 0 && this.#applyRegion(region, visible, plan)) {
|
|
245
|
+
changed.add(region);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return regions.filter((region) => changed.has(region)).map((region) => ({ region, visible: plan.get(region) === true }));
|
|
249
|
+
}
|
|
250
|
+
/** Reports logical transitions caused by user input or the public action. */
|
|
251
|
+
#commitChange(triggers = this.triggerTargets) {
|
|
252
|
+
for (const detail of this.#settle(triggers)) {
|
|
253
|
+
this.dispatch("change", { detail });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** Reports logical transitions derived from DOM, lifecycle, reset, or Value state. */
|
|
257
|
+
#reconcileDom() {
|
|
258
|
+
for (const detail of this.#settle()) {
|
|
259
|
+
this.dispatch("reconcile", { detail });
|
|
31
260
|
}
|
|
32
261
|
}
|
|
33
|
-
/** Applies
|
|
34
|
-
#applyRegion(region, visible) {
|
|
262
|
+
/** Applies one planned state while keeping event history separate from DOM repair. */
|
|
263
|
+
#applyRegion(region, visible, plan) {
|
|
35
264
|
const previous = this.#lastVisible.get(region) ?? !region.hidden;
|
|
36
|
-
|
|
265
|
+
const changed = visible !== previous;
|
|
37
266
|
this.#lastVisible.set(region, visible);
|
|
38
|
-
if (visible)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
267
|
+
if (!visible) this.#retreatFocus(region, plan);
|
|
268
|
+
this.#syncRegionState(region, visible);
|
|
269
|
+
this.#syncRegionControls(region, visible);
|
|
270
|
+
return changed;
|
|
271
|
+
}
|
|
272
|
+
/** Reflects visibility idempotently, recording writes so the observer ignores its own work. */
|
|
273
|
+
#syncRegionState(region, visible) {
|
|
274
|
+
this.#writeAttribute(region, "hidden", visible ? null : "");
|
|
275
|
+
this.#writeAttribute(region, "aria-hidden", visible ? null : "true");
|
|
276
|
+
this.#writeAttribute(region, "data-visible", visible ? "true" : null);
|
|
277
|
+
}
|
|
278
|
+
/** Enables/disables descendant controls, touching only state carrying our marker. */
|
|
279
|
+
#syncRegionControls(region, visible) {
|
|
280
|
+
const shouldDisable = !visible && this.disableHiddenValue;
|
|
281
|
+
const controls = region.querySelectorAll("input, textarea, select, button");
|
|
282
|
+
for (const control of controls) {
|
|
283
|
+
const marked = control.hasAttribute(DISABLED_MARKER);
|
|
284
|
+
if (shouldDisable) {
|
|
285
|
+
if (!control.disabled) {
|
|
286
|
+
this.#writeAttribute(control, "disabled", "");
|
|
287
|
+
this.#writeAttribute(control, DISABLED_MARKER, "true");
|
|
288
|
+
} else if (marked && control.getAttribute(DISABLED_MARKER) !== "true") {
|
|
289
|
+
this.#writeAttribute(control, DISABLED_MARKER, "true");
|
|
290
|
+
}
|
|
291
|
+
} else if (marked) {
|
|
292
|
+
this.#writeAttribute(control, "disabled", null);
|
|
293
|
+
this.#writeAttribute(control, DISABLED_MARKER, null);
|
|
294
|
+
}
|
|
52
295
|
}
|
|
53
296
|
}
|
|
54
297
|
/** Whether a region's declared condition holds across `triggers` per `match`. */
|
|
55
298
|
#isVisible(region, triggers) {
|
|
56
299
|
const predicate = this.#predicateFor(region);
|
|
57
300
|
if (predicate === null) return !region.hidden;
|
|
58
|
-
|
|
59
|
-
return this.matchValue === "all" ? triggers.every(predicate) : triggers.some(predicate);
|
|
301
|
+
return triggers.length > 0 && (this.matchValue === "all" ? triggers.every(predicate) : triggers.some(predicate));
|
|
60
302
|
}
|
|
61
|
-
/** Builds the per-trigger predicate from the region's
|
|
303
|
+
/** Builds the deterministic per-trigger predicate from the region's condition. */
|
|
62
304
|
#predicateFor(region) {
|
|
63
305
|
if (region.hasAttribute("data-when-checked")) {
|
|
64
306
|
return (trigger) => this.#isChecked(trigger);
|
|
@@ -75,35 +317,87 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
75
317
|
#isChecked(trigger) {
|
|
76
318
|
return trigger instanceof HTMLInputElement && trigger.checked;
|
|
77
319
|
}
|
|
78
|
-
/** A trigger
|
|
320
|
+
/** A trigger has a value when a radio/checkbox is selected, or its control value matches. */
|
|
79
321
|
#matchesValue(trigger, wanted) {
|
|
80
322
|
if (trigger instanceof HTMLInputElement && (trigger.type === "checkbox" || trigger.type === "radio")) {
|
|
81
323
|
return trigger.checked && trigger.value === wanted;
|
|
82
324
|
}
|
|
83
325
|
return trigger.value === wanted;
|
|
84
326
|
}
|
|
85
|
-
/**
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
327
|
+
/** Moves focus to the first safe trigger, or to the controller landmark as a fallback. */
|
|
328
|
+
#retreatFocus(region, plan) {
|
|
329
|
+
const active = document.activeElement;
|
|
330
|
+
if (active && region.contains(active)) this.#moveFocus(plan);
|
|
331
|
+
}
|
|
332
|
+
/** Moves focus after the caller establishes that the active node is being hidden. */
|
|
333
|
+
#moveFocus(plan) {
|
|
334
|
+
const destination = this.triggerTargets.find(
|
|
335
|
+
(trigger) => canTakeFocus(trigger) && this.#survivesPlan(trigger, plan)
|
|
336
|
+
);
|
|
337
|
+
if (destination) {
|
|
338
|
+
this.#rootTabindex.returnAll();
|
|
339
|
+
destination.focus();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.#rootTabindex.lend(this.element);
|
|
343
|
+
this.element.focus();
|
|
344
|
+
}
|
|
345
|
+
/** Whether a focus candidate sits outside every region this pass will hide. */
|
|
346
|
+
#survivesPlan(candidate, plan) {
|
|
347
|
+
return [...plan].every(([region, visible]) => visible || !region.contains(candidate));
|
|
348
|
+
}
|
|
349
|
+
/** Records one idempotent attribute write before MutationObserver sees it. */
|
|
350
|
+
#writeAttribute(element, name, value) {
|
|
351
|
+
if (element.getAttribute(name) !== value) {
|
|
352
|
+
if (this.#observing) {
|
|
353
|
+
let writes = this.#internalWrites.get(element);
|
|
354
|
+
if (!writes) {
|
|
355
|
+
writes = /* @__PURE__ */ new Map();
|
|
356
|
+
this.#internalWrites.set(element, writes);
|
|
94
357
|
}
|
|
95
|
-
|
|
96
|
-
control.disabled = false;
|
|
97
|
-
control.removeAttribute(DISABLED_MARKER);
|
|
358
|
+
writes.set(name, value);
|
|
98
359
|
}
|
|
360
|
+
if (value === null) element.removeAttribute(name);
|
|
361
|
+
else element.setAttribute(name, value);
|
|
99
362
|
}
|
|
100
363
|
}
|
|
101
|
-
/**
|
|
102
|
-
#
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
364
|
+
/** Separates authored records from controller writes and queues one relevant repair. */
|
|
365
|
+
#onMutations(records) {
|
|
366
|
+
const triggers = this.triggerTargets;
|
|
367
|
+
const regions = this.regionTargets;
|
|
368
|
+
const touchedWrites = /* @__PURE__ */ new Map();
|
|
369
|
+
let needsReconcile = false;
|
|
370
|
+
for (const record of records) {
|
|
371
|
+
if (record.type === "attributes" && record.target instanceof Element) {
|
|
372
|
+
const name = record.attributeName;
|
|
373
|
+
const writes = this.#internalWrites.get(record.target);
|
|
374
|
+
if (name && writes?.has(name)) {
|
|
375
|
+
let names = touchedWrites.get(record.target);
|
|
376
|
+
if (!names) {
|
|
377
|
+
names = /* @__PURE__ */ new Set();
|
|
378
|
+
touchedWrites.set(record.target, names);
|
|
379
|
+
}
|
|
380
|
+
names.add(name);
|
|
381
|
+
if (record.target.getAttribute(name) === writes.get(name)) continue;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (record.target instanceof Element && this.#elementAffectsState(record.target, triggers, regions)) {
|
|
385
|
+
needsReconcile = true;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
for (const [element, names] of touchedWrites) {
|
|
389
|
+
const writes = this.#internalWrites.get(element);
|
|
390
|
+
for (const name of names) writes?.delete(name);
|
|
391
|
+
}
|
|
392
|
+
if (needsReconcile) this.#reconcile.schedule();
|
|
393
|
+
}
|
|
394
|
+
/** Whether an authored mutation target belongs to a trigger, region, or managed control. */
|
|
395
|
+
#elementAffectsState(target, triggers, regions) {
|
|
396
|
+
return triggers.some((trigger) => trigger === target || trigger.contains(target)) || regions.some((region) => region === target || region.contains(target));
|
|
397
|
+
}
|
|
398
|
+
/** Whether a form owns at least one current trigger, including `form=` associations. */
|
|
399
|
+
#hasTriggerOwnedBy(form) {
|
|
400
|
+
return this.triggerTargets.some((trigger) => trigger.form === form);
|
|
107
401
|
}
|
|
108
402
|
};
|
|
109
403
|
|