stimeo-ui 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +209 -0
- data/README.md +128 -1
- data/dist/cable/index.js +392 -417
- data/dist/controllers/accordion_controller.js +228 -28
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -191
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +295 -110
- data/dist/controllers/avatar_controller.js +172 -106
- data/dist/controllers/breadcrumb_controller.js +0 -129
- data/dist/controllers/bulk_select_controller.js +264 -53
- data/dist/controllers/calendar_controller.js +346 -180
- data/dist/controllers/carousel_controller.js +587 -300
- data/dist/controllers/character_counter_controller.js +271 -143
- data/dist/controllers/checkbox_controller.js +77 -75
- data/dist/controllers/clipboard_controller.js +187 -108
- data/dist/controllers/collapsible_controller.js +374 -108
- data/dist/controllers/color_picker_controller.js +183 -127
- data/dist/controllers/combobox_controller.js +426 -109
- data/dist/controllers/command_palette_controller.js +994 -350
- data/dist/controllers/conditional_fields_controller.js +212 -125
- data/dist/controllers/confirm_controller.js +939 -245
- data/dist/controllers/context_menu_controller.js +275 -127
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +235 -97
- data/dist/controllers/currency_input_controller.js +241 -118
- data/dist/controllers/data_grid_controller.js +229 -135
- data/dist/controllers/date_range_picker_controller.js +359 -165
- data/dist/controllers/dialog_controller.js +887 -225
- data/dist/controllers/direct_upload_controller.js +80 -142
- data/dist/controllers/dirty_form_controller.js +26 -47
- data/dist/controllers/dismissible_controller.js +175 -20
- data/dist/controllers/drawer_controller.js +963 -336
- data/dist/controllers/dropdown_controller.js +342 -85
- data/dist/controllers/editable_controller.js +182 -58
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +426 -229
- data/dist/controllers/filter_controller.js +78 -36
- data/dist/controllers/flash_controller.js +328 -211
- data/dist/controllers/focus_controller.js +654 -239
- data/dist/controllers/form_field_controller.js +153 -136
- data/dist/controllers/form_validation_controller.js +29 -96
- data/dist/controllers/frame_loading_controller.js +263 -198
- data/dist/controllers/highlight_controller.js +119 -70
- data/dist/controllers/hover_card_controller.js +335 -118
- data/dist/controllers/idle_controller.js +316 -65
- data/dist/controllers/input_mask_controller.js +116 -61
- data/dist/controllers/intersection_controller.js +147 -103
- data/dist/controllers/lazy_frame_controller.js +9 -56
- data/dist/controllers/listbox_controller.js +413 -153
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -95
- data/dist/controllers/menu_controller.js +254 -167
- data/dist/controllers/menubar_controller.js +206 -331
- data/dist/controllers/meter_controller.js +159 -71
- data/dist/controllers/multi_select_controller.js +797 -388
- data/dist/controllers/navigation_menu_controller.js +156 -231
- data/dist/controllers/nested_form_controller.js +373 -174
- data/dist/controllers/network_status_controller.js +241 -45
- data/dist/controllers/number_input_controller.js +387 -227
- data/dist/controllers/optimistic_controller.js +28 -75
- data/dist/controllers/otp_controller.js +344 -196
- data/dist/controllers/overflow_indicator_controller.js +167 -93
- data/dist/controllers/overflow_menu_controller.js +408 -277
- data/dist/controllers/pagination_controller.js +342 -120
- data/dist/controllers/password_reveal_controller.js +424 -99
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +252 -96
- data/dist/controllers/pointer_drag_controller.js +263 -164
- data/dist/controllers/popover_controller.js +267 -94
- data/dist/controllers/portal_controller.js +209 -72
- data/dist/controllers/preview_guard_controller.js +243 -112
- data/dist/controllers/progress_controller.js +132 -59
- data/dist/controllers/radio_group_controller.js +138 -133
- data/dist/controllers/range_slider_controller.js +245 -112
- data/dist/controllers/rating_controller.js +429 -194
- data/dist/controllers/read_more_controller.js +185 -47
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +294 -118
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -221
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -146
- data/dist/controllers/scrollspy_controller.js +181 -236
- data/dist/controllers/separator_controller.js +288 -132
- data/dist/controllers/sidebar_controller.js +1071 -375
- data/dist/controllers/skeleton_controller.js +248 -92
- data/dist/controllers/slider_controller.js +211 -96
- data/dist/controllers/smart_sticky_header_controller.js +163 -54
- data/dist/controllers/sortable_controller.js +13 -110
- data/dist/controllers/spinner_controller.js +277 -161
- data/dist/controllers/step_indicator_controller.js +107 -53
- data/dist/controllers/stepper_controller.js +171 -48
- data/dist/controllers/stick_to_bottom_controller.js +310 -116
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -192
- data/dist/controllers/switch_controller.js +107 -43
- data/dist/controllers/tabs_controller.js +50 -25
- data/dist/controllers/tags_input_controller.js +290 -149
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +148 -80
- data/dist/controllers/time_picker_controller.js +162 -81
- data/dist/controllers/toast_controller.js +365 -193
- data/dist/controllers/toggle_group_controller.js +194 -99
- data/dist/controllers/toolbar_controller.js +92 -90
- data/dist/controllers/tooltip_controller.js +185 -125
- data/dist/controllers/transition_controller.js +123 -115
- data/dist/controllers/tree_view_controller.js +248 -290
- data/dist/index.js +8649 -8572
- data/dist/positioning/index.js +148 -45
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/submit_once_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/announce.ts
|
|
6
3
|
function announce(message, options = {}) {
|
|
7
4
|
const text = message.trim();
|
|
8
5
|
if (text.length === 0) return;
|
|
@@ -13,192 +10,265 @@ function announce(message, options = {}) {
|
|
|
13
10
|
);
|
|
14
11
|
}
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
function sharedRegistry(key, create) {
|
|
14
|
+
const symbol = Symbol.for(key);
|
|
15
|
+
const scope = globalThis;
|
|
16
|
+
const existing = scope[symbol];
|
|
17
|
+
if (existing !== void 0) return existing;
|
|
18
|
+
const registry = create();
|
|
19
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
20
|
+
return registry;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isAttributeValue(value) {
|
|
24
|
+
return value === null || typeof value === "string";
|
|
25
|
+
}
|
|
26
|
+
function readLeaseRecord(element, record) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
29
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
30
|
+
const [original, written, ...rest] = parsed;
|
|
31
|
+
const linked = rest[0] !== false;
|
|
32
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
33
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
34
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
35
|
+
} catch {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
var AttributeLease = class _AttributeLease {
|
|
40
|
+
static #registry = sharedRegistry(
|
|
41
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
42
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
43
|
+
);
|
|
18
44
|
#attribute;
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
45
|
+
#record;
|
|
46
|
+
#base;
|
|
47
|
+
#held = /* @__PURE__ */ new Set();
|
|
48
|
+
constructor(attribute, owner, options = {}) {
|
|
22
49
|
this.#attribute = attribute;
|
|
50
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
51
|
+
this.#base = options.base === true;
|
|
52
|
+
const names = _AttributeLease.#registry.names;
|
|
53
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
23
54
|
}
|
|
24
|
-
/** Writes or removes the leased attribute while preserving its authored value. */
|
|
25
55
|
write(element, value) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
this.#reflect(element, value);
|
|
56
|
+
const stack = this.#stack(element);
|
|
57
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
58
|
+
this.#held.add(element);
|
|
59
|
+
const shows = !this.#covered(element, stack, hold);
|
|
60
|
+
hold.written = value;
|
|
61
|
+
this.#mark(element, stack);
|
|
62
|
+
if (shows) this.#reflect(element, value);
|
|
36
63
|
}
|
|
37
|
-
/** Returns one lease without overwriting a value subsequently authored by a consumer. */
|
|
38
64
|
return(element) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
this.#
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
65
|
+
const held = this.#held.delete(element);
|
|
66
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
67
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
68
|
+
const hold = this.#take(stack);
|
|
69
|
+
if (!hold) return false;
|
|
70
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
71
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
72
|
+
const index = stack.indexOf(hold);
|
|
73
|
+
stack.splice(index, 1);
|
|
74
|
+
const above = stack[index];
|
|
75
|
+
if (above) above.linked &&= hold.linked;
|
|
76
|
+
this.#mark(element, stack);
|
|
77
|
+
if (!owns) return true;
|
|
78
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
79
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
returnAll() {
|
|
83
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
84
|
+
}
|
|
85
|
+
#stack(element) {
|
|
86
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
87
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
88
|
+
stacks.set(element, byAttribute);
|
|
89
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
90
|
+
byAttribute.set(this.#attribute, stack);
|
|
91
|
+
return stack;
|
|
92
|
+
}
|
|
93
|
+
#seed(element) {
|
|
94
|
+
const found = [];
|
|
95
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
96
|
+
const read = readLeaseRecord(element, record);
|
|
97
|
+
if (read) found.push({ ...read, record });
|
|
98
|
+
}
|
|
99
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
100
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
101
|
+
lease: null,
|
|
102
|
+
record,
|
|
103
|
+
written,
|
|
104
|
+
original,
|
|
105
|
+
linked: linked && index > 0
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
#take(stack) {
|
|
109
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
110
|
+
if (hold) hold.lease = this;
|
|
111
|
+
return hold;
|
|
112
|
+
}
|
|
113
|
+
#acquire(element, stack) {
|
|
114
|
+
const current = element.getAttribute(this.#attribute);
|
|
115
|
+
const top = stack.at(-1);
|
|
116
|
+
const bottom = stack[0];
|
|
117
|
+
const over = top?.written === current ? top : void 0;
|
|
118
|
+
const from = this.#base ? bottom : over;
|
|
119
|
+
const hold = {
|
|
120
|
+
lease: this,
|
|
121
|
+
record: this.#record,
|
|
122
|
+
written: current,
|
|
123
|
+
original: from ? from.original : current,
|
|
124
|
+
linked: !this.#base && over !== void 0
|
|
125
|
+
};
|
|
126
|
+
if (this.#base && bottom) {
|
|
127
|
+
bottom.linked = true;
|
|
128
|
+
stack.unshift(hold);
|
|
129
|
+
} else stack.push(hold);
|
|
130
|
+
return hold;
|
|
131
|
+
}
|
|
132
|
+
#covered(element, stack, hold) {
|
|
133
|
+
const current = element.getAttribute(this.#attribute);
|
|
134
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
135
|
+
}
|
|
136
|
+
#mark(element, stack) {
|
|
137
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
138
|
+
stack.forEach((hold, index) => {
|
|
139
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
140
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
141
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
142
|
+
records.set(
|
|
143
|
+
hold.record,
|
|
144
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
for (const [name, recorded] of records) {
|
|
148
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
149
|
+
if (recorded === null) element.removeAttribute(name);
|
|
150
|
+
else element.setAttribute(name, recorded);
|
|
151
|
+
}
|
|
44
152
|
}
|
|
45
|
-
/** Reflects only a real value transition, avoiding self-triggered mutation work. */
|
|
46
153
|
#reflect(element, value) {
|
|
47
154
|
if (element.getAttribute(this.#attribute) === value) return;
|
|
48
155
|
if (value === null) element.removeAttribute(this.#attribute);
|
|
49
156
|
else element.setAttribute(this.#attribute, value);
|
|
50
157
|
}
|
|
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
158
|
};
|
|
85
159
|
|
|
86
|
-
// src/utils/detach_gate.ts
|
|
87
160
|
var DetachGate = class _DetachGate {
|
|
88
|
-
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
89
161
|
#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
162
|
get pending() {
|
|
98
163
|
return this.#pending;
|
|
99
164
|
}
|
|
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
165
|
static isDetached(host) {
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
*/
|
|
166
|
+
return !host.element.isConnected || !_DetachGate.#listed(host);
|
|
167
|
+
}
|
|
117
168
|
disconnected(host, teardown) {
|
|
118
|
-
|
|
169
|
+
const carried = _DetachGate.#carried(host);
|
|
170
|
+
if (!carried && _DetachGate.isDetached(host)) {
|
|
119
171
|
this.#pending = false;
|
|
120
172
|
teardown();
|
|
121
173
|
return;
|
|
122
174
|
}
|
|
123
175
|
this.#pending = true;
|
|
124
|
-
|
|
176
|
+
const probe = () => {
|
|
125
177
|
if (!this.#pending) return;
|
|
126
178
|
this.#pending = false;
|
|
127
179
|
teardown();
|
|
128
|
-
}
|
|
180
|
+
};
|
|
181
|
+
if (carried) setTimeout(probe, 0);
|
|
182
|
+
else queueMicrotask(probe);
|
|
183
|
+
}
|
|
184
|
+
static #carried(host) {
|
|
185
|
+
const element = host.element;
|
|
186
|
+
return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
|
|
187
|
+
}
|
|
188
|
+
static #listed(host) {
|
|
189
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
190
|
+
return tokens.includes(host.identifier);
|
|
129
191
|
}
|
|
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
192
|
cancel() {
|
|
137
193
|
this.#pending = false;
|
|
138
194
|
}
|
|
139
195
|
};
|
|
140
196
|
|
|
141
|
-
// src/utils/listener_set.ts
|
|
142
197
|
var ListenerSet = class {
|
|
143
|
-
/** The generation every `add` joins until the next `dispose()`. */
|
|
144
198
|
#abort = new AbortController();
|
|
145
|
-
/**
|
|
146
|
-
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
147
|
-
*
|
|
148
|
-
* The set supplies the signal, so `options` carries everything else the DOM
|
|
149
|
-
* accepts — `capture` included, which has to match at release time and no
|
|
150
|
-
* longer has a second place to drift from.
|
|
151
|
-
*/
|
|
152
199
|
add(target, type, handler, options) {
|
|
153
200
|
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
154
201
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Releases every listener of the open generation, synchronously, and opens the
|
|
157
|
-
* next one. Idempotent, and safe before anything has been added.
|
|
158
|
-
*/
|
|
159
202
|
dispose() {
|
|
160
203
|
this.#abort.abort();
|
|
161
204
|
this.#abort = new AbortController();
|
|
162
205
|
}
|
|
163
206
|
};
|
|
164
207
|
|
|
165
|
-
|
|
208
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
209
|
+
|
|
210
|
+
var NUMBER_BOUNDS = {
|
|
211
|
+
timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
|
|
212
|
+
function matchesNumberBounds(value, bounds) {
|
|
213
|
+
if (!Number.isFinite(value)) {
|
|
214
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
215
|
+
if (direction === null) return false;
|
|
216
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
217
|
+
}
|
|
218
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
219
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
220
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
221
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
222
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function readNumber(raw, fallback, bounds) {
|
|
227
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
var NumberValueReader = class {
|
|
231
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
232
|
+
read(owner, name, raw, fallback, bounds) {
|
|
233
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
234
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
235
|
+
this.#lastRejected.delete(name);
|
|
236
|
+
return resolved;
|
|
237
|
+
}
|
|
238
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
239
|
+
const literal = owner.element.getAttribute(attribute);
|
|
240
|
+
if (literal === null) {
|
|
241
|
+
this.#lastRejected.delete(name);
|
|
242
|
+
return resolved;
|
|
243
|
+
}
|
|
244
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
245
|
+
this.#lastRejected.set(name, literal);
|
|
246
|
+
console.warn(
|
|
247
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return resolved;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
166
254
|
var TimerRegistry = class {
|
|
167
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
168
255
|
ids = /* @__PURE__ */ new Set();
|
|
169
|
-
/**
|
|
170
|
-
* Cancels a single tracked timer.
|
|
171
|
-
*
|
|
172
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
173
|
-
* registry), so callers can clear defensively without guarding.
|
|
174
|
-
*/
|
|
175
256
|
clear(id) {
|
|
176
257
|
if (this.ids.delete(id)) {
|
|
177
258
|
this.cancel(id);
|
|
178
259
|
}
|
|
179
260
|
}
|
|
180
|
-
/**
|
|
181
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
182
|
-
* to guarantee no timer outlives the element.
|
|
183
|
-
*/
|
|
184
261
|
clearAll() {
|
|
185
262
|
for (const id of this.ids) {
|
|
186
263
|
this.cancel(id);
|
|
187
264
|
}
|
|
188
265
|
this.ids.clear();
|
|
189
266
|
}
|
|
190
|
-
/** Number of timers currently tracked (pending). */
|
|
191
267
|
get size() {
|
|
192
268
|
return this.ids.size;
|
|
193
269
|
}
|
|
194
270
|
};
|
|
195
271
|
var SafeTimeout = class extends TimerRegistry {
|
|
196
|
-
/**
|
|
197
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
198
|
-
*
|
|
199
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
200
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
201
|
-
*/
|
|
202
272
|
set(callback, delay) {
|
|
203
273
|
const id = this.schedule(() => {
|
|
204
274
|
this.ids.delete(id);
|
|
@@ -215,9 +285,11 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
215
285
|
}
|
|
216
286
|
};
|
|
217
287
|
|
|
218
|
-
// src/controllers/submit_once_controller.ts
|
|
219
288
|
var BUTTON_BUSY_LABEL = "data-submit-once-busy-label";
|
|
220
|
-
var
|
|
289
|
+
var LABEL_RECORD = "label";
|
|
290
|
+
var SUBMITTER_MARK = "submitter";
|
|
291
|
+
var SubmitOnceController = class _SubmitOnceController extends Controller {
|
|
292
|
+
#numbers = new NumberValueReader();
|
|
221
293
|
static targets = ["submit", "idle", "busy"];
|
|
222
294
|
static values = {
|
|
223
295
|
announceText: { type: String, default: "" },
|
|
@@ -226,26 +298,27 @@ var SubmitOnceController = class extends Controller {
|
|
|
226
298
|
timeout: { type: Number, default: 0 },
|
|
227
299
|
restoreFocus: { type: Boolean, default: false }
|
|
228
300
|
};
|
|
301
|
+
static valueConstraints = {
|
|
302
|
+
timeout: NUMBER_BOUNDS.timer
|
|
303
|
+
};
|
|
229
304
|
static actions = ["cancel", "finish", "start"];
|
|
230
305
|
static events = ["start", "end", "reconcile"];
|
|
231
306
|
#timers = new SafeTimeout();
|
|
232
307
|
#gate = new DetachGate();
|
|
233
308
|
#listeners = new ListenerSet();
|
|
234
|
-
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
235
309
|
#sessions = /* @__PURE__ */ new Map();
|
|
236
|
-
#disabled = new AttributeLease("disabled");
|
|
237
|
-
#controlBusy = new AttributeLease("aria-busy");
|
|
238
|
-
#formBusy = new AttributeLease("aria-busy");
|
|
239
|
-
#submitting = new AttributeLease("data-submitting");
|
|
240
|
-
#ariaLabel = new AttributeLease("aria-label");
|
|
241
|
-
#hidden = new AttributeLease("hidden");
|
|
310
|
+
#disabled = new AttributeLease("disabled", this.identifier);
|
|
311
|
+
#controlBusy = new AttributeLease("aria-busy", this.identifier);
|
|
312
|
+
#formBusy = new AttributeLease("aria-busy", this.identifier);
|
|
313
|
+
#submitting = new AttributeLease("data-submitting", this.identifier);
|
|
314
|
+
#ariaLabel = new AttributeLease("aria-label", this.identifier);
|
|
315
|
+
#hidden = new AttributeLease("hidden", this.identifier);
|
|
242
316
|
#onSubmitStart = (event) => {
|
|
243
317
|
this.start(event);
|
|
244
318
|
};
|
|
245
319
|
#onSubmitEnd = (event) => {
|
|
246
320
|
this.#complete(this.#eventForm(event), "turbo", this.#eventSuccess(event));
|
|
247
321
|
};
|
|
248
|
-
/** Blocks a later native submit while one for the same form is still active. */
|
|
249
322
|
#onNativeSubmit = (event) => {
|
|
250
323
|
const session = this.#sessionFor(event.target);
|
|
251
324
|
if (!session) return;
|
|
@@ -254,24 +327,80 @@ var SubmitOnceController = class extends Controller {
|
|
|
254
327
|
};
|
|
255
328
|
connect() {
|
|
256
329
|
this.#gate.cancel();
|
|
257
|
-
this.#beforeCache.activate();
|
|
258
330
|
this.#listeners.add(this.element, "submit", this.#onNativeSubmit, { capture: true });
|
|
259
331
|
this.#listeners.add(this.element, "turbo:submit-start", this.#onSubmitStart);
|
|
260
332
|
this.#listeners.add(this.element, "turbo:submit-end", this.#onSubmitEnd);
|
|
333
|
+
if (this.#sessions.size === 0) this.#releaseInherited();
|
|
261
334
|
}
|
|
262
335
|
disconnect() {
|
|
263
336
|
this.#listeners.dispose();
|
|
264
|
-
this.#beforeCache.deactivate();
|
|
265
337
|
this.#gate.disconnected(this, () => this.#teardown());
|
|
266
338
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
339
|
+
get #labelRecord() {
|
|
340
|
+
return `data-${this.identifier}-${LABEL_RECORD}`;
|
|
341
|
+
}
|
|
342
|
+
get #submitterMark() {
|
|
343
|
+
return `data-${this.identifier}-${SUBMITTER_MARK}`;
|
|
344
|
+
}
|
|
345
|
+
#releaseInherited() {
|
|
346
|
+
const forms = this.#ownedForms();
|
|
347
|
+
const controls = new Set(
|
|
348
|
+
this.element.querySelectorAll(`[${this.#labelRecord}]`)
|
|
349
|
+
);
|
|
350
|
+
for (const form of forms) {
|
|
351
|
+
for (const control of this.#submitControls(form)) {
|
|
352
|
+
if (control.hasAttribute(this.#labelRecord)) controls.add(control);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
for (const control of controls) {
|
|
356
|
+
if (control.form && !this.#ownsForm(control.form)) continue;
|
|
357
|
+
this.#restoreLabel(control);
|
|
358
|
+
}
|
|
359
|
+
const copied = forms.filter((form) => this.#returnCopied(form));
|
|
360
|
+
if (copied.length > 0) this.dispatch("reconcile", { detail: { forms: copied } });
|
|
361
|
+
}
|
|
362
|
+
#returnCopied(form) {
|
|
363
|
+
let copied = this.#submitting.return(form);
|
|
364
|
+
this.#formBusy.return(form);
|
|
365
|
+
const parts = [...this.idleTargets, ...this.busyTargets];
|
|
366
|
+
for (const control of this.#submitControls(form)) {
|
|
367
|
+
this.#disabled.return(control);
|
|
368
|
+
this.#controlBusy.return(control);
|
|
369
|
+
if (control instanceof HTMLButtonElement) this.#ariaLabel.return(control);
|
|
370
|
+
for (const part of parts) if (control.contains(part)) this.#hidden.return(part);
|
|
371
|
+
if (!control.hasAttribute(this.#submitterMark)) continue;
|
|
372
|
+
control.removeAttribute(this.#submitterMark);
|
|
373
|
+
control.disabled = false;
|
|
374
|
+
if (control.getAttribute("aria-disabled") === "true")
|
|
375
|
+
control.removeAttribute("aria-disabled");
|
|
376
|
+
copied = true;
|
|
377
|
+
}
|
|
378
|
+
if (copied && form.getAttribute("aria-busy") === "true") form.removeAttribute("aria-busy");
|
|
379
|
+
return copied;
|
|
380
|
+
}
|
|
381
|
+
#restoreLabel(control) {
|
|
382
|
+
const raw = control.getAttribute(this.#labelRecord);
|
|
383
|
+
control.removeAttribute(this.#labelRecord);
|
|
384
|
+
let record;
|
|
385
|
+
try {
|
|
386
|
+
record = JSON.parse(raw ?? "");
|
|
387
|
+
} catch {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (!Array.isArray(record) || record.length !== 2) return;
|
|
391
|
+
const [original, written] = record;
|
|
392
|
+
if (typeof original !== "string" || typeof written !== "string") return;
|
|
393
|
+
if (control instanceof HTMLInputElement) {
|
|
394
|
+
if (control.value === written) control.value = original;
|
|
395
|
+
} else if (control.childElementCount === 0 && control.textContent === written) {
|
|
396
|
+
control.textContent = original;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
#ownedForms() {
|
|
400
|
+
const forms = Array.from(this.element.querySelectorAll("form"));
|
|
401
|
+
if (this.element instanceof HTMLFormElement) forms.unshift(this.element);
|
|
402
|
+
return forms.filter((form) => this.#ownsForm(form));
|
|
403
|
+
}
|
|
275
404
|
start(event) {
|
|
276
405
|
if (event.defaultPrevented) return;
|
|
277
406
|
const form = this.#eventForm(event);
|
|
@@ -289,49 +418,33 @@ var SubmitOnceController = class extends Controller {
|
|
|
289
418
|
busyPart: null
|
|
290
419
|
};
|
|
291
420
|
this.#sessions.set(form, session);
|
|
421
|
+
submitter?.setAttribute(this.#submitterMark, "");
|
|
292
422
|
this.#formBusy.write(form, "true");
|
|
293
423
|
this.#submitting.write(form, "true");
|
|
294
424
|
this.#syncControls(session, controls);
|
|
295
425
|
this.#enterLabel(session);
|
|
296
426
|
this.dispatch("start", { detail: { form, submitter } });
|
|
297
|
-
if (this
|
|
427
|
+
if (this.#safeTimeout > 0) {
|
|
298
428
|
session.timeoutId = this.#timers.set(
|
|
299
429
|
() => this.#complete(form, "timeout", false),
|
|
300
|
-
this
|
|
430
|
+
this.#safeTimeout
|
|
301
431
|
);
|
|
302
432
|
}
|
|
303
433
|
announce(this.announceTextValue);
|
|
304
434
|
}
|
|
305
|
-
/**
|
|
306
|
-
* Completes a non-Turbo async submission for the form associated with
|
|
307
|
-
* `event`. When called directly with no event, it completes the controller's
|
|
308
|
-
* sole active form (or the controller element itself when it is a form).
|
|
309
|
-
*/
|
|
310
435
|
finish(event) {
|
|
311
436
|
const form = event ? this.#eventForm(event) : this.#defaultForm();
|
|
312
437
|
this.#complete(form, "manual", event ? this.#eventSuccess(event) : void 0);
|
|
313
438
|
}
|
|
314
|
-
/**
|
|
315
|
-
* Abandons the submission for the form associated with `event` because it
|
|
316
|
-
* never reached the network — a veto from validation, or a request the
|
|
317
|
-
* consumer gave up before sending. With no event it targets the controller's
|
|
318
|
-
* sole active form, exactly like {@link finish}.
|
|
319
|
-
*
|
|
320
|
-
* The distinction from `finish` is observable: a request that ran and failed
|
|
321
|
-
* is `finish` with `success: false` and still announces completion, while a
|
|
322
|
-
* submission that never happened announces nothing.
|
|
323
|
-
*/
|
|
324
439
|
cancel(event) {
|
|
325
440
|
const form = event ? this.#eventForm(event) : this.#defaultForm();
|
|
326
441
|
this.#complete(form, "canceled", false);
|
|
327
442
|
}
|
|
328
|
-
/** Disables a newly connected explicit submit target during its form's session. */
|
|
329
443
|
submitTargetConnected(target) {
|
|
330
444
|
const session = this.#sessionFor(target.form);
|
|
331
445
|
if (!session || !this.#isNativeSubmitControl(target)) return;
|
|
332
446
|
this.#enterControl(session, target);
|
|
333
447
|
}
|
|
334
|
-
/** Ends one session, restores owned state, and reports the observable completion. */
|
|
335
448
|
#complete(form, reason, success) {
|
|
336
449
|
const session = this.#sessionFor(form);
|
|
337
450
|
if (!session) return;
|
|
@@ -342,18 +455,15 @@ var SubmitOnceController = class extends Controller {
|
|
|
342
455
|
this.#restoreFocus(session);
|
|
343
456
|
if (reason !== "canceled") announce(this.announceReadyTextValue);
|
|
344
457
|
}
|
|
345
|
-
/** Applies the busy state to every current native/explicit control exactly once. */
|
|
346
458
|
#syncControls(session, controls = this.#submitControls(session.form)) {
|
|
347
459
|
for (const control of controls) this.#enterControl(session, control);
|
|
348
460
|
}
|
|
349
|
-
/** Temporarily disables one enabled submit control while retaining authored state. */
|
|
350
461
|
#enterControl(session, control) {
|
|
351
462
|
if (session.controls.has(control) || control.matches(":disabled")) return;
|
|
352
463
|
session.controls.add(control);
|
|
353
464
|
this.#disabled.write(control, "");
|
|
354
465
|
this.#controlBusy.write(control, "true");
|
|
355
466
|
}
|
|
356
|
-
/** Swaps only the triggering control's safe label channel. */
|
|
357
467
|
#enterLabel(session) {
|
|
358
468
|
const control = session.submitter;
|
|
359
469
|
if (!control) return;
|
|
@@ -375,6 +485,7 @@ var SubmitOnceController = class extends Controller {
|
|
|
375
485
|
original: control.value,
|
|
376
486
|
written: label
|
|
377
487
|
};
|
|
488
|
+
control.setAttribute(this.#labelRecord, JSON.stringify([control.value, label]));
|
|
378
489
|
control.value = label;
|
|
379
490
|
return;
|
|
380
491
|
}
|
|
@@ -389,24 +500,27 @@ var SubmitOnceController = class extends Controller {
|
|
|
389
500
|
original: control.textContent ?? "",
|
|
390
501
|
written: label
|
|
391
502
|
};
|
|
503
|
+
control.setAttribute(this.#labelRecord, JSON.stringify([control.textContent ?? "", label]));
|
|
392
504
|
control.textContent = label;
|
|
393
505
|
}
|
|
394
|
-
|
|
395
|
-
#returnSessionState(session) {
|
|
506
|
+
#returnSessionState(session, unfinished = false) {
|
|
396
507
|
if (session.timeoutId !== null) {
|
|
397
508
|
this.#timers.clear(session.timeoutId);
|
|
398
509
|
session.timeoutId = null;
|
|
399
510
|
}
|
|
400
511
|
if (session.labelWrite?.channel === "value") {
|
|
401
512
|
const { control, original, written } = session.labelWrite;
|
|
513
|
+
control.removeAttribute(this.#labelRecord);
|
|
402
514
|
if (control.value === written) control.value = original;
|
|
403
515
|
} else if (session.labelWrite?.channel === "text") {
|
|
404
516
|
const { control, original, written } = session.labelWrite;
|
|
517
|
+
control.removeAttribute(this.#labelRecord);
|
|
405
518
|
if (control.childElementCount === 0 && control.textContent === written) {
|
|
406
519
|
control.textContent = original;
|
|
407
520
|
}
|
|
408
521
|
}
|
|
409
522
|
session.labelWrite = null;
|
|
523
|
+
if (!unfinished) session.submitter?.removeAttribute(this.#submitterMark);
|
|
410
524
|
if (session.submitter instanceof HTMLButtonElement) {
|
|
411
525
|
this.#ariaLabel.return(session.submitter);
|
|
412
526
|
}
|
|
@@ -418,11 +532,9 @@ var SubmitOnceController = class extends Controller {
|
|
|
418
532
|
this.#controlBusy.return(control);
|
|
419
533
|
this.#disabled.return(control);
|
|
420
534
|
}
|
|
421
|
-
session.controls.clear();
|
|
422
535
|
this.#submitting.return(session.form);
|
|
423
536
|
this.#formBusy.return(session.form);
|
|
424
537
|
}
|
|
425
|
-
/** Restores focus only when disabling the focused submitter actually stranded it. */
|
|
426
538
|
#restoreFocus(session) {
|
|
427
539
|
const submitter = session.submitter;
|
|
428
540
|
if (!this.restoreFocusValue || !session.submitterHadFocus || !submitter?.isConnected) return;
|
|
@@ -431,27 +543,18 @@ var SubmitOnceController = class extends Controller {
|
|
|
431
543
|
submitter.focus();
|
|
432
544
|
}
|
|
433
545
|
}
|
|
434
|
-
/** Silently abandons every session before caching or on a real detach. */
|
|
435
546
|
#abandonSessions() {
|
|
436
547
|
this.#timers.clearAll();
|
|
548
|
+
const departed = !this.element.isConnected;
|
|
437
549
|
for (const session of this.#sessions.values()) {
|
|
438
550
|
session.timeoutId = null;
|
|
439
|
-
this.#returnSessionState(session);
|
|
551
|
+
this.#returnSessionState(session, departed);
|
|
440
552
|
}
|
|
441
553
|
this.#sessions.clear();
|
|
442
554
|
}
|
|
443
|
-
/** Keeps Turbo's cached snapshot idle, without events, announcements, or focus moves. */
|
|
444
|
-
#rewindForCache() {
|
|
445
|
-
const forms = [...this.#sessions.keys()];
|
|
446
|
-
this.#abandonSessions();
|
|
447
|
-
if (forms.length > 0) this.dispatch("reconcile", { detail: { forms } });
|
|
448
|
-
}
|
|
449
|
-
/** Tears down a true detach; an in-page move is canceled by the next `connect()`. */
|
|
450
555
|
#teardown() {
|
|
451
|
-
this.#gate.cancel();
|
|
452
556
|
this.#abandonSessions();
|
|
453
557
|
}
|
|
454
|
-
/** All native controls associated with `form`, plus valid explicit targets. */
|
|
455
558
|
#submitControls(form) {
|
|
456
559
|
const controls = /* @__PURE__ */ new Set();
|
|
457
560
|
for (const element of Array.from(form.elements)) {
|
|
@@ -463,13 +566,11 @@ var SubmitOnceController = class extends Controller {
|
|
|
463
566
|
}
|
|
464
567
|
return Array.from(controls);
|
|
465
568
|
}
|
|
466
|
-
/** Resolves the real submitter, falling back to the first enabled submit control. */
|
|
467
569
|
#resolveSubmitter(event, controls) {
|
|
468
570
|
const submitter = this.#eventSubmitter(event);
|
|
469
571
|
if (submitter && controls.includes(submitter)) return submitter;
|
|
470
572
|
return controls.find((control) => !control.matches(":disabled")) ?? controls[0] ?? null;
|
|
471
573
|
}
|
|
472
|
-
/** Reads a native or Turbo submitter without assuming `SubmitEvent` exists. */
|
|
473
574
|
#eventSubmitter(event) {
|
|
474
575
|
if (typeof SubmitEvent !== "undefined" && event instanceof SubmitEvent) {
|
|
475
576
|
return this.#isNativeSubmitControl(event.submitter) ? event.submitter : null;
|
|
@@ -478,21 +579,18 @@ var SubmitOnceController = class extends Controller {
|
|
|
478
579
|
const submitter = detail?.formSubmission?.submitter;
|
|
479
580
|
return this.#isNativeSubmitControl(submitter) ? submitter : null;
|
|
480
581
|
}
|
|
481
|
-
/** Finds the form represented by an action/Turbo event and verifies this scope owns it. */
|
|
482
582
|
#eventForm(event) {
|
|
483
583
|
const detail = event.detail;
|
|
484
584
|
const target = event.target;
|
|
485
585
|
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);
|
|
486
586
|
return candidate && this.#ownsForm(candidate) ? candidate : null;
|
|
487
587
|
}
|
|
488
|
-
/** The only unambiguous form for a direct `finish()` call. */
|
|
489
588
|
#defaultForm() {
|
|
490
589
|
if (this.element instanceof HTMLFormElement && this.#ownsForm(this.element))
|
|
491
590
|
return this.element;
|
|
492
591
|
if (this.#sessions.size === 1) return this.#sessions.keys().next().value ?? null;
|
|
493
592
|
return null;
|
|
494
593
|
}
|
|
495
|
-
/** Prevents an ancestor controller from also taking over a nested instance's form. */
|
|
496
594
|
#ownsForm(form) {
|
|
497
595
|
let candidate = form;
|
|
498
596
|
while (candidate) {
|
|
@@ -502,29 +600,33 @@ var SubmitOnceController = class extends Controller {
|
|
|
502
600
|
}
|
|
503
601
|
return false;
|
|
504
602
|
}
|
|
505
|
-
/** Returns the active session for a form-shaped external event value. */
|
|
506
603
|
#sessionFor(candidate) {
|
|
507
604
|
if (!(candidate instanceof HTMLFormElement)) return void 0;
|
|
508
605
|
return this.#sessions.get(candidate);
|
|
509
606
|
}
|
|
510
|
-
/** Effective native submit controls, including implicit buttons and image inputs. */
|
|
511
607
|
#isNativeSubmitControl(element) {
|
|
512
608
|
if (element instanceof HTMLButtonElement) return element.type === "submit";
|
|
513
609
|
return element instanceof HTMLInputElement && (element.type === "submit" || element.type === "image");
|
|
514
610
|
}
|
|
515
|
-
/** Finds a structured label part belonging to this button, not a nested control. */
|
|
516
611
|
#partInside(control, parts) {
|
|
517
612
|
return parts.find(
|
|
518
613
|
(part) => control.contains(part) && part.closest("button, input[type=submit], input[type=image]") === control
|
|
519
614
|
) ?? null;
|
|
520
615
|
}
|
|
521
|
-
/** Reads an optional boolean `success` from Turbo or a manual completion event. */
|
|
522
616
|
#eventSuccess(event) {
|
|
523
617
|
const success = event.detail?.success;
|
|
524
618
|
return typeof success === "boolean" ? success : void 0;
|
|
525
619
|
}
|
|
620
|
+
get #safeTimeout() {
|
|
621
|
+
return this.#numbers.read(
|
|
622
|
+
this,
|
|
623
|
+
"timeout",
|
|
624
|
+
this.timeoutValue,
|
|
625
|
+
_SubmitOnceController.values.timeout.default,
|
|
626
|
+
_SubmitOnceController.valueConstraints.timeout
|
|
627
|
+
);
|
|
628
|
+
}
|
|
526
629
|
};
|
|
527
630
|
|
|
528
631
|
export { SubmitOnceController };
|
|
529
|
-
//# sourceMappingURL=submit_once_controller.js.map
|
|
530
632
|
//# sourceMappingURL=submit_once_controller.js.map
|