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
|
@@ -2,6 +2,142 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/submit_once_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/announce.ts
|
|
6
|
+
function announce(message, options = {}) {
|
|
7
|
+
const text = message.trim();
|
|
8
|
+
if (text.length === 0) return;
|
|
9
|
+
window.dispatchEvent(
|
|
10
|
+
new CustomEvent("stimeo--announcer:announce", {
|
|
11
|
+
detail: { message: text, assertive: options.assertive === true }
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/utils/attribute_lease.ts
|
|
17
|
+
var AttributeLease = class {
|
|
18
|
+
#attribute;
|
|
19
|
+
#records = /* @__PURE__ */ new Map();
|
|
20
|
+
/** @param attribute - The attribute whose temporary values this lease owns. */
|
|
21
|
+
constructor(attribute) {
|
|
22
|
+
this.#attribute = attribute;
|
|
23
|
+
}
|
|
24
|
+
/** Writes or removes the leased attribute while preserving its authored value. */
|
|
25
|
+
write(element, value) {
|
|
26
|
+
const existing = this.#records.get(element);
|
|
27
|
+
if (existing) {
|
|
28
|
+
existing.written = value;
|
|
29
|
+
} else {
|
|
30
|
+
this.#records.set(element, {
|
|
31
|
+
original: element.getAttribute(this.#attribute),
|
|
32
|
+
written: value
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
this.#reflect(element, value);
|
|
36
|
+
}
|
|
37
|
+
/** Returns one lease without overwriting a value subsequently authored by a consumer. */
|
|
38
|
+
return(element) {
|
|
39
|
+
const record = this.#records.get(element);
|
|
40
|
+
if (!record) return;
|
|
41
|
+
this.#records.delete(element);
|
|
42
|
+
const stillOwned = element.getAttribute(this.#attribute) === record.written;
|
|
43
|
+
if (stillOwned) this.#reflect(element, record.original);
|
|
44
|
+
}
|
|
45
|
+
/** Reflects only a real value transition, avoiding self-triggered mutation work. */
|
|
46
|
+
#reflect(element, value) {
|
|
47
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
48
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
49
|
+
else element.setAttribute(this.#attribute, value);
|
|
50
|
+
}
|
|
51
|
+
/** Returns every outstanding lease using the same ownership check as {@link return}. */
|
|
52
|
+
returnAll() {
|
|
53
|
+
for (const element of Array.from(this.#records.keys())) this.return(element);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/utils/before_cache_reset.ts
|
|
58
|
+
var BeforeCacheReset = class _BeforeCacheReset {
|
|
59
|
+
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
60
|
+
static #subscribers = /* @__PURE__ */ new Set();
|
|
61
|
+
/** The shared listener; installed while at least one instance is subscribed. */
|
|
62
|
+
static #onBeforeCache = () => {
|
|
63
|
+
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
64
|
+
};
|
|
65
|
+
#rewind;
|
|
66
|
+
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
67
|
+
constructor(rewind) {
|
|
68
|
+
this.#rewind = rewind;
|
|
69
|
+
}
|
|
70
|
+
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
71
|
+
activate() {
|
|
72
|
+
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
73
|
+
_BeforeCacheReset.#subscribers.add(this);
|
|
74
|
+
if (first) {
|
|
75
|
+
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
79
|
+
deactivate() {
|
|
80
|
+
_BeforeCacheReset.#subscribers.delete(this);
|
|
81
|
+
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
82
|
+
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/utils/detach_gate.ts
|
|
87
|
+
var DetachGate = class _DetachGate {
|
|
88
|
+
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
89
|
+
#pending = false;
|
|
90
|
+
/**
|
|
91
|
+
* True while a probe is queued — the last disconnect was ambiguous and no
|
|
92
|
+
* reconnect has cancelled it yet. Read it from `connect()` to tell the
|
|
93
|
+
* reconnect half of an in-page move from a first connect: a controller whose
|
|
94
|
+
* initialisation restarts a measurement (a min-duration floor, an elapsed
|
|
95
|
+
* counter) must skip it for the move, where nothing actually restarted.
|
|
96
|
+
*/
|
|
97
|
+
get pending() {
|
|
98
|
+
return this.#pending;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* True when the disconnect is definitely a real detach — the element left
|
|
102
|
+
* the document, or `data-controller` no longer lists the identifier. False
|
|
103
|
+
* means ambiguous (in-page move or observed-root exit), NOT "alive".
|
|
104
|
+
*/
|
|
105
|
+
static isDetached(host) {
|
|
106
|
+
if (!host.element.isConnected) return true;
|
|
107
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
108
|
+
return !tokens.includes(host.identifier);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Call from `disconnect()`: runs `teardown` synchronously on a definite
|
|
112
|
+
* detach (fast path), otherwise defers it one microtask — a reconnect
|
|
113
|
+
* ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
|
|
114
|
+
* One microtask is the whole probe window: Stimulus reconnects a moved
|
|
115
|
+
* element within the same mutation batch, before the checkpoint drains.
|
|
116
|
+
*/
|
|
117
|
+
disconnected(host, teardown) {
|
|
118
|
+
if (_DetachGate.isDetached(host)) {
|
|
119
|
+
this.#pending = false;
|
|
120
|
+
teardown();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.#pending = true;
|
|
124
|
+
queueMicrotask(() => {
|
|
125
|
+
if (!this.#pending) return;
|
|
126
|
+
this.#pending = false;
|
|
127
|
+
teardown();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Disarms a pending probe. Call from `connect()` (the reconnect that proves
|
|
132
|
+
* an in-page move) and from the head of any teardown path not routed through
|
|
133
|
+
* {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
|
|
134
|
+
* never run the teardown a second time.
|
|
135
|
+
*/
|
|
136
|
+
cancel() {
|
|
137
|
+
this.#pending = false;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
5
141
|
// src/utils/safe_timeout.ts
|
|
6
142
|
var TimerRegistry = class {
|
|
7
143
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -56,169 +192,311 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
56
192
|
};
|
|
57
193
|
|
|
58
194
|
// src/controllers/submit_once_controller.ts
|
|
59
|
-
var ORIGINAL_LABEL = "data-submit-once-original-label";
|
|
60
|
-
var DISABLED_MARKER = "data-submit-once-disabled";
|
|
61
195
|
var BUTTON_BUSY_LABEL = "data-submit-once-busy-label";
|
|
62
196
|
var SubmitOnceController = class extends Controller {
|
|
63
|
-
static targets = ["submit"];
|
|
197
|
+
static targets = ["submit", "idle", "busy"];
|
|
64
198
|
static values = {
|
|
199
|
+
announceText: { type: String, default: "" },
|
|
200
|
+
announceReadyText: { type: String, default: "" },
|
|
65
201
|
busyLabel: { type: String, default: "" },
|
|
66
202
|
timeout: { type: Number, default: 0 },
|
|
67
203
|
restoreFocus: { type: Boolean, default: false }
|
|
68
204
|
};
|
|
69
|
-
static actions = ["start"];
|
|
205
|
+
static actions = ["cancel", "finish", "start"];
|
|
70
206
|
static events = ["start", "end"];
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
207
|
+
#timers = new SafeTimeout();
|
|
208
|
+
#gate = new DetachGate();
|
|
209
|
+
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
210
|
+
#sessions = /* @__PURE__ */ new Map();
|
|
211
|
+
#disabled = new AttributeLease("disabled");
|
|
212
|
+
#controlBusy = new AttributeLease("aria-busy");
|
|
213
|
+
#formBusy = new AttributeLease("aria-busy");
|
|
214
|
+
#submitting = new AttributeLease("data-submitting");
|
|
215
|
+
#ariaLabel = new AttributeLease("aria-label");
|
|
216
|
+
#hidden = new AttributeLease("hidden");
|
|
75
217
|
#onSubmitStart = (event) => {
|
|
76
218
|
this.start(event);
|
|
77
219
|
};
|
|
78
220
|
#onSubmitEnd = (event) => {
|
|
79
|
-
|
|
80
|
-
|
|
221
|
+
this.#complete(this.#eventForm(event), "turbo", this.#eventSuccess(event));
|
|
222
|
+
};
|
|
223
|
+
/** Blocks a later native submit while one for the same form is still active. */
|
|
224
|
+
#onNativeSubmit = (event) => {
|
|
225
|
+
const session = this.#sessionFor(event.target);
|
|
226
|
+
if (!session) return;
|
|
227
|
+
this.#syncControls(session);
|
|
228
|
+
event.preventDefault();
|
|
81
229
|
};
|
|
82
230
|
connect() {
|
|
231
|
+
this.#gate.cancel();
|
|
232
|
+
this.#beforeCache.activate();
|
|
233
|
+
this.element.addEventListener("submit", this.#onNativeSubmit, true);
|
|
83
234
|
this.element.addEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
84
235
|
this.element.addEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
85
|
-
this.#clearStaleBusy();
|
|
86
236
|
}
|
|
87
237
|
disconnect() {
|
|
238
|
+
this.element.removeEventListener("submit", this.#onNativeSubmit, true);
|
|
88
239
|
this.element.removeEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
89
240
|
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
90
|
-
this.#
|
|
241
|
+
this.#beforeCache.deactivate();
|
|
242
|
+
this.#gate.disconnected(this, () => this.#teardown());
|
|
91
243
|
}
|
|
92
244
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* `
|
|
245
|
+
* Begins the submission represented by `event`.
|
|
246
|
+
*
|
|
247
|
+
* Native `submit` events are intended for non-Turbo async forms. Turbo calls
|
|
248
|
+
* this path through its auto-subscribed `turbo:submit-start` event. An event
|
|
249
|
+
* whose default an earlier listener already cancelled carries no submission,
|
|
250
|
+
* so it starts nothing.
|
|
96
251
|
*/
|
|
97
252
|
start(event) {
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
253
|
+
if (event.defaultPrevented) return;
|
|
254
|
+
const form = this.#eventForm(event);
|
|
255
|
+
if (!form || this.#sessions.has(form)) return;
|
|
256
|
+
const controls = this.#submitControls(form);
|
|
257
|
+
const submitter = this.#resolveSubmitter(event, controls);
|
|
258
|
+
const session = {
|
|
259
|
+
form,
|
|
260
|
+
submitter,
|
|
261
|
+
submitterHadFocus: submitter !== null && document.activeElement === submitter,
|
|
262
|
+
controls: /* @__PURE__ */ new Set(),
|
|
263
|
+
timeoutId: null,
|
|
264
|
+
labelWrite: null,
|
|
265
|
+
idlePart: null,
|
|
266
|
+
busyPart: null
|
|
267
|
+
};
|
|
268
|
+
this.#sessions.set(form, session);
|
|
269
|
+
this.#formBusy.write(form, "true");
|
|
270
|
+
this.#submitting.write(form, "true");
|
|
271
|
+
this.#syncControls(session, controls);
|
|
272
|
+
this.#enterLabel(session);
|
|
273
|
+
this.dispatch("start", { detail: { form, submitter } });
|
|
109
274
|
if (this.timeoutValue > 0) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
275
|
+
session.timeoutId = this.#timers.set(
|
|
276
|
+
() => this.#complete(form, "timeout", false),
|
|
277
|
+
this.timeoutValue
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
announce(this.announceTextValue);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Completes a non-Turbo async submission for the form associated with
|
|
284
|
+
* `event`. When called directly with no event, it completes the controller's
|
|
285
|
+
* sole active form (or the controller element itself when it is a form).
|
|
286
|
+
*/
|
|
287
|
+
finish(event) {
|
|
288
|
+
const form = event ? this.#eventForm(event) : this.#defaultForm();
|
|
289
|
+
this.#complete(form, "manual", event ? this.#eventSuccess(event) : void 0);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Abandons the submission for the form associated with `event` because it
|
|
293
|
+
* never reached the network — a veto from validation, or a request the
|
|
294
|
+
* consumer gave up before sending. With no event it targets the controller's
|
|
295
|
+
* sole active form, exactly like {@link finish}.
|
|
296
|
+
*
|
|
297
|
+
* The distinction from `finish` is observable: a request that ran and failed
|
|
298
|
+
* is `finish` with `success: false` and still announces completion, while a
|
|
299
|
+
* submission that never happened announces nothing.
|
|
300
|
+
*/
|
|
301
|
+
cancel(event) {
|
|
302
|
+
const form = event ? this.#eventForm(event) : this.#defaultForm();
|
|
303
|
+
this.#complete(form, "canceled", false);
|
|
304
|
+
}
|
|
305
|
+
/** Disables a newly connected explicit submit target during its form's session. */
|
|
306
|
+
submitTargetConnected(target) {
|
|
307
|
+
const session = this.#sessionFor(target.form);
|
|
308
|
+
if (!session || !this.#isNativeSubmitControl(target)) return;
|
|
309
|
+
this.#enterControl(session, target);
|
|
310
|
+
}
|
|
311
|
+
/** Ends one session, restores owned state, and reports the observable completion. */
|
|
312
|
+
#complete(form, reason, success) {
|
|
313
|
+
const session = this.#sessionFor(form);
|
|
314
|
+
if (!session) return;
|
|
315
|
+
this.#sessions.delete(session.form);
|
|
316
|
+
this.#returnSessionState(session);
|
|
317
|
+
const detail = success === void 0 ? { form: session.form, submitter: session.submitter, reason } : { form: session.form, submitter: session.submitter, reason, success };
|
|
318
|
+
this.dispatch("end", { detail });
|
|
319
|
+
this.#restoreFocus(session);
|
|
320
|
+
if (reason !== "canceled") announce(this.announceReadyTextValue);
|
|
321
|
+
}
|
|
322
|
+
/** Applies the busy state to every current native/explicit control exactly once. */
|
|
323
|
+
#syncControls(session, controls = this.#submitControls(session.form)) {
|
|
324
|
+
for (const control of controls) this.#enterControl(session, control);
|
|
325
|
+
}
|
|
326
|
+
/** Temporarily disables one enabled submit control while retaining authored state. */
|
|
327
|
+
#enterControl(session, control) {
|
|
328
|
+
if (session.controls.has(control) || control.matches(":disabled")) return;
|
|
329
|
+
session.controls.add(control);
|
|
330
|
+
this.#disabled.write(control, "");
|
|
331
|
+
this.#controlBusy.write(control, "true");
|
|
332
|
+
}
|
|
333
|
+
/** Swaps only the triggering control's safe label channel. */
|
|
334
|
+
#enterLabel(session) {
|
|
335
|
+
const control = session.submitter;
|
|
336
|
+
if (!control) return;
|
|
337
|
+
const idlePart = this.#partInside(control, this.idleTargets);
|
|
338
|
+
const busyPart = this.#partInside(control, this.busyTargets);
|
|
339
|
+
if (idlePart && busyPart) {
|
|
340
|
+
session.idlePart = idlePart;
|
|
341
|
+
session.busyPart = busyPart;
|
|
342
|
+
this.#hidden.write(idlePart, "");
|
|
343
|
+
this.#hidden.write(busyPart, null);
|
|
344
|
+
}
|
|
345
|
+
const label = control.getAttribute(BUTTON_BUSY_LABEL) ?? this.busyLabelValue;
|
|
138
346
|
if (label.length === 0) return;
|
|
139
|
-
if (
|
|
140
|
-
|
|
347
|
+
if (control instanceof HTMLInputElement) {
|
|
348
|
+
if (control.type !== "submit") return;
|
|
349
|
+
session.labelWrite = {
|
|
350
|
+
channel: "value",
|
|
351
|
+
control,
|
|
352
|
+
original: control.value,
|
|
353
|
+
written: label
|
|
354
|
+
};
|
|
355
|
+
control.value = label;
|
|
356
|
+
return;
|
|
141
357
|
}
|
|
142
|
-
|
|
358
|
+
if (control.hasAttribute("aria-label")) {
|
|
359
|
+
this.#ariaLabel.write(control, label);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (control.childElementCount > 0) return;
|
|
363
|
+
session.labelWrite = {
|
|
364
|
+
channel: "text",
|
|
365
|
+
control,
|
|
366
|
+
original: control.textContent ?? "",
|
|
367
|
+
written: label
|
|
368
|
+
};
|
|
369
|
+
control.textContent = label;
|
|
143
370
|
}
|
|
144
|
-
/**
|
|
145
|
-
#
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
371
|
+
/** Returns labels, controls, and form hooks, respecting intervening consumer writes. */
|
|
372
|
+
#returnSessionState(session) {
|
|
373
|
+
if (session.timeoutId !== null) {
|
|
374
|
+
this.#timers.clear(session.timeoutId);
|
|
375
|
+
session.timeoutId = null;
|
|
376
|
+
}
|
|
377
|
+
if (session.labelWrite?.channel === "value") {
|
|
378
|
+
const { control, original, written } = session.labelWrite;
|
|
379
|
+
if (control.value === written) control.value = original;
|
|
380
|
+
} else if (session.labelWrite?.channel === "text") {
|
|
381
|
+
const { control, original, written } = session.labelWrite;
|
|
382
|
+
if (control.childElementCount === 0 && control.textContent === written) {
|
|
383
|
+
control.textContent = original;
|
|
384
|
+
}
|
|
150
385
|
}
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
this.#
|
|
154
|
-
button.removeAttribute(ORIGINAL_LABEL);
|
|
386
|
+
session.labelWrite = null;
|
|
387
|
+
if (session.submitter instanceof HTMLButtonElement) {
|
|
388
|
+
this.#ariaLabel.return(session.submitter);
|
|
155
389
|
}
|
|
390
|
+
if (session.idlePart) this.#hidden.return(session.idlePart);
|
|
391
|
+
if (session.busyPart) this.#hidden.return(session.busyPart);
|
|
392
|
+
session.idlePart = null;
|
|
393
|
+
session.busyPart = null;
|
|
394
|
+
for (const control of session.controls) {
|
|
395
|
+
this.#controlBusy.return(control);
|
|
396
|
+
this.#disabled.return(control);
|
|
397
|
+
}
|
|
398
|
+
session.controls.clear();
|
|
399
|
+
this.#submitting.return(session.form);
|
|
400
|
+
this.#formBusy.return(session.form);
|
|
156
401
|
}
|
|
157
|
-
/**
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
if (this.
|
|
161
|
-
|
|
162
|
-
|
|
402
|
+
/** Restores focus only when disabling the focused submitter actually stranded it. */
|
|
403
|
+
#restoreFocus(session) {
|
|
404
|
+
const submitter = session.submitter;
|
|
405
|
+
if (!this.restoreFocusValue || !session.submitterHadFocus || !submitter?.isConnected) return;
|
|
406
|
+
const active = document.activeElement;
|
|
407
|
+
if (active === null || active === document.body || active === document.documentElement) {
|
|
408
|
+
submitter.focus();
|
|
163
409
|
}
|
|
164
|
-
|
|
165
|
-
|
|
410
|
+
}
|
|
411
|
+
/** Silently abandons every session before caching or on a real detach. */
|
|
412
|
+
#abandonSessions() {
|
|
413
|
+
this.#timers.clearAll();
|
|
414
|
+
for (const session of this.#sessions.values()) {
|
|
415
|
+
session.timeoutId = null;
|
|
416
|
+
this.#returnSessionState(session);
|
|
166
417
|
}
|
|
418
|
+
this.#sessions.clear();
|
|
167
419
|
}
|
|
168
|
-
/**
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
420
|
+
/** Keeps Turbo's cached snapshot idle, without events, announcements, or focus moves. */
|
|
421
|
+
#rewindForCache() {
|
|
422
|
+
this.#abandonSessions();
|
|
423
|
+
}
|
|
424
|
+
/** Tears down a true detach; an in-page move is canceled by the next `connect()`. */
|
|
425
|
+
#teardown() {
|
|
426
|
+
this.#gate.cancel();
|
|
427
|
+
this.#abandonSessions();
|
|
428
|
+
}
|
|
429
|
+
/** All native controls associated with `form`, plus valid explicit targets. */
|
|
430
|
+
#submitControls(form) {
|
|
431
|
+
const controls = /* @__PURE__ */ new Set();
|
|
432
|
+
for (const element of Array.from(form.elements)) {
|
|
433
|
+
if (this.#isNativeSubmitControl(element)) controls.add(element);
|
|
434
|
+
}
|
|
435
|
+
const root = form.getRootNode();
|
|
436
|
+
for (const image of root.querySelectorAll('input[type="image"]')) {
|
|
437
|
+
if (image.form === form) controls.add(image);
|
|
173
438
|
}
|
|
174
|
-
return
|
|
439
|
+
return Array.from(controls);
|
|
175
440
|
}
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
441
|
+
/** Resolves the real submitter, falling back to the first enabled submit control. */
|
|
442
|
+
#resolveSubmitter(event, controls) {
|
|
443
|
+
const submitter = this.#eventSubmitter(event);
|
|
444
|
+
if (submitter && controls.includes(submitter)) return submitter;
|
|
445
|
+
return controls.find((control) => !control.matches(":disabled")) ?? controls[0] ?? null;
|
|
446
|
+
}
|
|
447
|
+
/** Reads a native or Turbo submitter without assuming `SubmitEvent` exists. */
|
|
181
448
|
#eventSubmitter(event) {
|
|
182
449
|
if (typeof SubmitEvent !== "undefined" && event instanceof SubmitEvent) {
|
|
183
|
-
return event.submitter;
|
|
450
|
+
return this.#isNativeSubmitControl(event.submitter) ? event.submitter : null;
|
|
184
451
|
}
|
|
185
452
|
const detail = event.detail;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
#isSubmitButton(el) {
|
|
196
|
-
return el instanceof HTMLButtonElement || el instanceof HTMLInputElement;
|
|
197
|
-
}
|
|
198
|
-
/** Reads a button's visible label (input value, aria-label, or text). */
|
|
199
|
-
#getLabel(button) {
|
|
200
|
-
if (button instanceof HTMLInputElement) return button.value;
|
|
201
|
-
const aria = button.getAttribute("aria-label");
|
|
202
|
-
if (aria !== null) return aria;
|
|
203
|
-
return button.textContent ?? "";
|
|
204
|
-
}
|
|
205
|
-
/** Writes a button's visible label through the same channel it was read from. */
|
|
206
|
-
#setLabel(button, text) {
|
|
207
|
-
if (button instanceof HTMLInputElement) {
|
|
208
|
-
button.value = text;
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (button.hasAttribute("aria-label")) {
|
|
212
|
-
button.setAttribute("aria-label", text);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
button.textContent = text;
|
|
453
|
+
const submitter = detail?.formSubmission?.submitter;
|
|
454
|
+
return this.#isNativeSubmitControl(submitter) ? submitter : null;
|
|
455
|
+
}
|
|
456
|
+
/** Finds the form represented by an action/Turbo event and verifies this scope owns it. */
|
|
457
|
+
#eventForm(event) {
|
|
458
|
+
const detail = event.detail;
|
|
459
|
+
const target = event.target;
|
|
460
|
+
const candidate = (target instanceof HTMLFormElement ? target : null) ?? (target instanceof Element ? target.closest("form") : null) ?? detail?.formSubmission?.formElement ?? detail?.form ?? this.#eventSubmitter(event)?.form ?? (this.element instanceof HTMLFormElement ? this.element : null);
|
|
461
|
+
return candidate && this.#ownsForm(candidate) ? candidate : null;
|
|
216
462
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
this
|
|
463
|
+
/** The only unambiguous form for a direct `finish()` call. */
|
|
464
|
+
#defaultForm() {
|
|
465
|
+
if (this.element instanceof HTMLFormElement && this.#ownsForm(this.element))
|
|
466
|
+
return this.element;
|
|
467
|
+
if (this.#sessions.size === 1) return this.#sessions.keys().next().value ?? null;
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
/** Prevents an ancestor controller from also taking over a nested instance's form. */
|
|
471
|
+
#ownsForm(form) {
|
|
472
|
+
let candidate = form;
|
|
473
|
+
while (candidate) {
|
|
474
|
+
const identifiers = (candidate.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
475
|
+
if (identifiers.includes(this.identifier)) return candidate === this.element;
|
|
476
|
+
candidate = candidate.parentElement;
|
|
221
477
|
}
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
/** Returns the active session for a form-shaped external event value. */
|
|
481
|
+
#sessionFor(candidate) {
|
|
482
|
+
if (!(candidate instanceof HTMLFormElement)) return void 0;
|
|
483
|
+
return this.#sessions.get(candidate);
|
|
484
|
+
}
|
|
485
|
+
/** Effective native submit controls, including implicit buttons and image inputs. */
|
|
486
|
+
#isNativeSubmitControl(element) {
|
|
487
|
+
if (element instanceof HTMLButtonElement) return element.type === "submit";
|
|
488
|
+
return element instanceof HTMLInputElement && (element.type === "submit" || element.type === "image");
|
|
489
|
+
}
|
|
490
|
+
/** Finds a structured label part belonging to this button, not a nested control. */
|
|
491
|
+
#partInside(control, parts) {
|
|
492
|
+
return parts.find(
|
|
493
|
+
(part) => control.contains(part) && part.closest("button, input[type=submit], input[type=image]") === control
|
|
494
|
+
) ?? null;
|
|
495
|
+
}
|
|
496
|
+
/** Reads an optional boolean `success` from Turbo or a manual completion event. */
|
|
497
|
+
#eventSuccess(event) {
|
|
498
|
+
const success = event.detail?.success;
|
|
499
|
+
return typeof success === "boolean" ? success : void 0;
|
|
222
500
|
}
|
|
223
501
|
};
|
|
224
502
|
|