stimeo-ui 0.16.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 +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- 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 +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
4
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
5
|
+
function stateReasonFor(event) {
|
|
6
|
+
if (!event) return "api";
|
|
7
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
8
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
9
|
+
return "user";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function actionSource(source) {
|
|
13
|
+
if (source instanceof HTMLElement) {
|
|
14
|
+
return { event: null, host: source, origin: source, reason: "api" };
|
|
15
|
+
}
|
|
16
|
+
const event = source ?? null;
|
|
17
|
+
return {
|
|
18
|
+
event,
|
|
19
|
+
host: event?.currentTarget instanceof Element ? event.currentTarget : null,
|
|
20
|
+
origin: event?.target instanceof Element ? event.target : null,
|
|
21
|
+
reason: stateReasonFor(event)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
4
24
|
|
|
5
|
-
// src/utils/event_owner.ts
|
|
6
25
|
function ownerIndex(candidates, node) {
|
|
7
26
|
if (!(node instanceof Node)) return -1;
|
|
8
27
|
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
@@ -11,51 +30,33 @@ function ownerOf(candidates, node) {
|
|
|
11
30
|
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
12
31
|
}
|
|
13
32
|
|
|
14
|
-
// src/utils/listener_set.ts
|
|
15
33
|
var ListenerSet = class {
|
|
16
|
-
/** The generation every `add` joins until the next `dispose()`. */
|
|
17
34
|
#abort = new AbortController();
|
|
18
|
-
/**
|
|
19
|
-
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
20
|
-
*
|
|
21
|
-
* The set supplies the signal, so `options` carries everything else the DOM
|
|
22
|
-
* accepts — `capture` included, which has to match at release time and no
|
|
23
|
-
* longer has a second place to drift from.
|
|
24
|
-
*/
|
|
25
35
|
add(target, type, handler, options) {
|
|
26
36
|
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
27
37
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Releases every listener of the open generation, synchronously, and opens the
|
|
30
|
-
* next one. Idempotent, and safe before anything has been added.
|
|
31
|
-
*/
|
|
32
38
|
dispose() {
|
|
33
39
|
this.#abort.abort();
|
|
34
40
|
this.#abort = new AbortController();
|
|
35
41
|
}
|
|
36
42
|
};
|
|
37
43
|
|
|
38
|
-
// src/utils/microtask_coalescer.ts
|
|
39
44
|
var MicrotaskCoalescer = class {
|
|
40
45
|
#run;
|
|
41
46
|
#queued = false;
|
|
42
47
|
#active = false;
|
|
43
48
|
#generation = 0;
|
|
44
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
45
49
|
constructor(run) {
|
|
46
50
|
this.#run = run;
|
|
47
51
|
}
|
|
48
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
49
52
|
activate() {
|
|
50
53
|
this.#active = true;
|
|
51
54
|
}
|
|
52
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
53
55
|
cancel() {
|
|
54
56
|
this.#active = false;
|
|
55
57
|
this.#queued = false;
|
|
56
58
|
this.#generation += 1;
|
|
57
59
|
}
|
|
58
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
59
60
|
schedule() {
|
|
60
61
|
if (!this.#active || this.#queued) return;
|
|
61
62
|
this.#queued = true;
|
|
@@ -68,43 +69,96 @@ var MicrotaskCoalescer = class {
|
|
|
68
69
|
}
|
|
69
70
|
};
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
var MorphRenderWatcher = class {
|
|
73
|
+
#listeners = new ListenerSet();
|
|
74
|
+
#pass;
|
|
75
|
+
#element = null;
|
|
76
|
+
#onMorph = () => this.#pass.schedule();
|
|
77
|
+
constructor(run) {
|
|
78
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
79
|
+
}
|
|
80
|
+
observe(element) {
|
|
81
|
+
if (this.#element === element) return;
|
|
82
|
+
this.disconnect();
|
|
83
|
+
this.#element = element;
|
|
84
|
+
this.#pass.activate();
|
|
85
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
86
|
+
}
|
|
87
|
+
schedule() {
|
|
88
|
+
this.#pass.schedule();
|
|
89
|
+
}
|
|
90
|
+
disconnect() {
|
|
91
|
+
this.#listeners.dispose();
|
|
92
|
+
this.#pass.cancel();
|
|
93
|
+
this.#element = null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
98
|
+
|
|
99
|
+
var NUMBER_BOUNDS = {
|
|
100
|
+
finite: { finite: true },
|
|
101
|
+
timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
|
|
102
|
+
function matchesNumberBounds(value, bounds) {
|
|
103
|
+
if (!Number.isFinite(value)) {
|
|
104
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
105
|
+
if (direction === null) return false;
|
|
106
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
107
|
+
}
|
|
108
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
109
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
110
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
111
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
112
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readNumber(raw, fallback, bounds) {
|
|
117
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
var NumberValueReader = class {
|
|
121
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
122
|
+
read(owner, name, raw, fallback, bounds) {
|
|
123
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
124
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
125
|
+
this.#lastRejected.delete(name);
|
|
126
|
+
return resolved;
|
|
127
|
+
}
|
|
128
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
129
|
+
const literal = owner.element.getAttribute(attribute);
|
|
130
|
+
if (literal === null) {
|
|
131
|
+
this.#lastRejected.delete(name);
|
|
132
|
+
return resolved;
|
|
133
|
+
}
|
|
134
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
135
|
+
this.#lastRejected.set(name, literal);
|
|
136
|
+
console.warn(
|
|
137
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return resolved;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
72
144
|
var TimerRegistry = class {
|
|
73
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
74
145
|
ids = /* @__PURE__ */ new Set();
|
|
75
|
-
/**
|
|
76
|
-
* Cancels a single tracked timer.
|
|
77
|
-
*
|
|
78
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
79
|
-
* registry), so callers can clear defensively without guarding.
|
|
80
|
-
*/
|
|
81
146
|
clear(id) {
|
|
82
147
|
if (this.ids.delete(id)) {
|
|
83
148
|
this.cancel(id);
|
|
84
149
|
}
|
|
85
150
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
88
|
-
* to guarantee no timer outlives the element.
|
|
89
|
-
*/
|
|
90
151
|
clearAll() {
|
|
91
152
|
for (const id of this.ids) {
|
|
92
153
|
this.cancel(id);
|
|
93
154
|
}
|
|
94
155
|
this.ids.clear();
|
|
95
156
|
}
|
|
96
|
-
/** Number of timers currently tracked (pending). */
|
|
97
157
|
get size() {
|
|
98
158
|
return this.ids.size;
|
|
99
159
|
}
|
|
100
160
|
};
|
|
101
161
|
var SafeTimeout = class extends TimerRegistry {
|
|
102
|
-
/**
|
|
103
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
104
|
-
*
|
|
105
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
106
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
107
|
-
*/
|
|
108
162
|
set(callback, delay) {
|
|
109
163
|
const id = this.schedule(() => {
|
|
110
164
|
this.ids.delete(id);
|
|
@@ -121,16 +175,9 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
121
175
|
}
|
|
122
176
|
};
|
|
123
177
|
|
|
124
|
-
// src/utils/keyed_timers.ts
|
|
125
178
|
var KeyedTimers = class {
|
|
126
179
|
#timers = new SafeTimeout();
|
|
127
|
-
/** The pending timer of each key; an entry lives exactly as long as its timer. */
|
|
128
180
|
#pending = /* @__PURE__ */ new Map();
|
|
129
|
-
/**
|
|
130
|
-
* Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had
|
|
131
|
-
* pending. The entry is dropped before the callback runs, so the callback sees
|
|
132
|
-
* the key unarmed and may arm it again for the next round.
|
|
133
|
-
*/
|
|
134
181
|
set(key, callback, delay) {
|
|
135
182
|
this.clear(key);
|
|
136
183
|
const id = this.#timers.set(() => {
|
|
@@ -139,52 +186,28 @@ var KeyedTimers = class {
|
|
|
139
186
|
}, delay);
|
|
140
187
|
this.#pending.set(key, id);
|
|
141
188
|
}
|
|
142
|
-
/**
|
|
143
|
-
* Cancels `key`'s pending timer, if it has one.
|
|
144
|
-
*
|
|
145
|
-
* A timer id is a positive integer, so `-1` stands for "nothing pending" and
|
|
146
|
-
* the registry ignores an id it does not own — the unarmed case needs no
|
|
147
|
-
* branch of its own, and no other key's timer can be reached from here.
|
|
148
|
-
*/
|
|
149
189
|
clear(key) {
|
|
150
190
|
this.#timers.clear(this.#pending.get(key) ?? -1);
|
|
151
191
|
this.#pending.delete(key);
|
|
152
192
|
}
|
|
153
|
-
/**
|
|
154
|
-
* Cancels every pending timer and forgets every key. Call this from a
|
|
155
|
-
* controller's `disconnect()` so no timer, and no entry, outlives the element.
|
|
156
|
-
*/
|
|
157
193
|
clearAll() {
|
|
158
194
|
this.#timers.clearAll();
|
|
159
195
|
this.#pending.clear();
|
|
160
196
|
}
|
|
161
|
-
/** Whether `key` has a timer pending. */
|
|
162
197
|
has(key) {
|
|
163
198
|
return this.#pending.has(key);
|
|
164
199
|
}
|
|
165
200
|
};
|
|
166
201
|
|
|
167
|
-
// src/utils/pausable_timers.ts
|
|
168
202
|
var PausableTimers = class {
|
|
169
203
|
#timers = new KeyedTimers();
|
|
170
|
-
/** Every key armed or held; an entry goes as its timer fires, or with the key. */
|
|
171
204
|
#entries = /* @__PURE__ */ new Map();
|
|
172
|
-
/**
|
|
173
|
-
* Arms `callback` after `delay` ms for `key`, replacing whatever `key` had.
|
|
174
|
-
* A held key stays held and banks `delay` for its resume.
|
|
175
|
-
*/
|
|
176
205
|
set(key, callback, delay) {
|
|
177
206
|
const reasons = this.#entries.get(key)?.reasons ?? /* @__PURE__ */ new Set();
|
|
178
207
|
const entry = { callback, startedAt: 0, remaining: delay, reasons };
|
|
179
208
|
this.#entries.set(key, entry);
|
|
180
209
|
if (reasons.size === 0) this.#arm(key, entry, callback);
|
|
181
210
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Holds `key` for `reason` and banks the time left, with a floor of one
|
|
184
|
-
* millisecond. A key with no timer is held all the same. Reports whether this
|
|
185
|
-
* call is the one that stopped a running timer, so the caller can write its
|
|
186
|
-
* held-state hook exactly once.
|
|
187
|
-
*/
|
|
188
211
|
pause(key, reason) {
|
|
189
212
|
let entry = this.#entries.get(key);
|
|
190
213
|
if (entry === void 0) {
|
|
@@ -197,11 +220,6 @@ var PausableTimers = class {
|
|
|
197
220
|
entry.remaining = Math.max(1, entry.remaining - (Date.now() - entry.startedAt));
|
|
198
221
|
return true;
|
|
199
222
|
}
|
|
200
|
-
/**
|
|
201
|
-
* Releases `reason` on `key`, arming the banked time again once no reason is
|
|
202
|
-
* left; a key released with no timer is forgotten. Reports whether this call
|
|
203
|
-
* is the one that started the timer again.
|
|
204
|
-
*/
|
|
205
223
|
resume(key, reason) {
|
|
206
224
|
const entry = this.#entries.get(key);
|
|
207
225
|
if (entry === void 0) return false;
|
|
@@ -215,38 +233,20 @@ var PausableTimers = class {
|
|
|
215
233
|
this.#arm(key, entry, callback);
|
|
216
234
|
return true;
|
|
217
235
|
}
|
|
218
|
-
/** Whether any reason holds `key`, with or without a timer. */
|
|
219
236
|
isHeld(key) {
|
|
220
237
|
return (this.#entries.get(key)?.reasons.size ?? 0) > 0;
|
|
221
238
|
}
|
|
222
|
-
/** Whether `key` is armed or held — that is, whether this registry drives it at all. */
|
|
223
239
|
tracks(key) {
|
|
224
240
|
return this.#entries.has(key);
|
|
225
241
|
}
|
|
226
|
-
/**
|
|
227
|
-
* Cancels `key`'s timer and keeps its hold, so a release arms nothing; a key
|
|
228
|
-
* nothing holds is forgotten.
|
|
229
|
-
*/
|
|
230
|
-
disarm(key) {
|
|
231
|
-
this.#timers.clear(key);
|
|
232
|
-
const entry = this.#entries.get(key);
|
|
233
|
-
if (entry?.reasons.size) entry.callback = null;
|
|
234
|
-
else this.#entries.delete(key);
|
|
235
|
-
}
|
|
236
|
-
/** Cancels `key`'s timer and drops its hold. */
|
|
237
242
|
clear(key) {
|
|
238
243
|
this.#timers.clear(key);
|
|
239
244
|
this.#entries.delete(key);
|
|
240
245
|
}
|
|
241
|
-
/**
|
|
242
|
-
* Cancels every timer and forgets every key. Call this from a controller's
|
|
243
|
-
* `disconnect()` so neither a timer nor a hold outlives the element.
|
|
244
|
-
*/
|
|
245
246
|
clearAll() {
|
|
246
247
|
this.#timers.clearAll();
|
|
247
248
|
this.#entries.clear();
|
|
248
249
|
}
|
|
249
|
-
/** Starts `entry`'s banked time running for `key`, and drops it as `callback` fires. */
|
|
250
250
|
#arm(key, entry, callback) {
|
|
251
251
|
entry.startedAt = Date.now();
|
|
252
252
|
this.#timers.set(
|
|
@@ -260,18 +260,15 @@ var PausableTimers = class {
|
|
|
260
260
|
}
|
|
261
261
|
};
|
|
262
262
|
|
|
263
|
-
// src/utils/target_selector.ts
|
|
264
263
|
function targetSelector(identifier, name) {
|
|
265
264
|
return `[data-${identifier}-target~="${name}"]`;
|
|
266
265
|
}
|
|
267
266
|
|
|
268
|
-
// src/utils/template_row.ts
|
|
269
267
|
function cloneTemplateRoot(template) {
|
|
270
268
|
const root = template.content.firstElementChild;
|
|
271
269
|
return root instanceof HTMLElement ? root.cloneNode(true) : null;
|
|
272
270
|
}
|
|
273
271
|
|
|
274
|
-
// src/utils/transition_completion.ts
|
|
275
272
|
function timeMs(value) {
|
|
276
273
|
const trimmed = value.trim();
|
|
277
274
|
const amount = Number.parseFloat(trimmed);
|
|
@@ -305,62 +302,49 @@ function maxTransitionTotalMs(style) {
|
|
|
305
302
|
return maxTotalMs(transitionTimings(style));
|
|
306
303
|
}
|
|
307
304
|
|
|
308
|
-
// src/controllers/toast_controller.ts
|
|
309
305
|
var DELEGATED_EVENTS = ["click", "focusin", "focusout", "keydown", "mouseover", "mouseout"];
|
|
310
|
-
var ToastController = class extends Controller {
|
|
306
|
+
var ToastController = class _ToastController extends Controller {
|
|
307
|
+
#numbers = new NumberValueReader();
|
|
311
308
|
static targets = ["list", "template", "item"];
|
|
312
309
|
static values = {
|
|
313
310
|
duration: { type: Number, default: 0 },
|
|
314
311
|
max: { type: Number, default: 3 }
|
|
315
312
|
};
|
|
313
|
+
static valueConstraints = {
|
|
314
|
+
duration: NUMBER_BOUNDS.timer,
|
|
315
|
+
max: NUMBER_BOUNDS.finite
|
|
316
|
+
};
|
|
316
317
|
static actions = ["dismiss", "onKeydown", "pause", "resume", "show"];
|
|
317
318
|
static events = ["dismiss", "show"];
|
|
318
|
-
/** Removal timers for the leaving transition; the auto-dismiss ones live below. */
|
|
319
319
|
#timers = new SafeTimeout();
|
|
320
|
-
/** Per-toast auto-dismiss, held open while the toast is hovered or focused. */
|
|
321
320
|
#dismiss = new PausableTimers();
|
|
322
|
-
/** Toasts taken on and not let go; a move within the list keeps a toast here. */
|
|
323
321
|
#taken = /* @__PURE__ */ new Set();
|
|
324
|
-
|
|
325
|
-
#
|
|
326
|
-
|
|
322
|
+
#outside = /* @__PURE__ */ new WeakSet();
|
|
323
|
+
#phases = /* @__PURE__ */ new WeakMap();
|
|
324
|
+
#reapply = new MorphRenderWatcher(() => this.#reapplyLimit());
|
|
327
325
|
#spared = [];
|
|
328
|
-
/** The next pointer movement, listened for while a hover hold waits on it. */
|
|
329
326
|
#pointer = new ListenerSet();
|
|
330
|
-
/** Toasts whose hover hold waits for the next pointer movement to be confirmed. */
|
|
331
327
|
#awaitingPointer = /* @__PURE__ */ new Set();
|
|
332
|
-
/**
|
|
333
|
-
* Pending one-shot `requestAnimationFrame` handles (the entering→visible flip).
|
|
334
|
-
* Tracked so {@link disconnect} can cancel any that have not fired, preventing a
|
|
335
|
-
* detached element from being mutated after it leaves the DOM (Turbo).
|
|
336
|
-
*/
|
|
337
328
|
#rafHandles = /* @__PURE__ */ new Map();
|
|
338
|
-
/** The stable list that owns delegated listeners for dynamically added items. */
|
|
339
329
|
#delegatedList = null;
|
|
340
|
-
/** Whether the controller is between `connect()` and `disconnect()`. */
|
|
341
330
|
#connected = false;
|
|
342
|
-
|
|
343
|
-
* The `max` the list was last held to, or `null` before the first time. `connect()`
|
|
344
|
-
* applies the limit only when `max` is not that one, so a reconnect of the same instance
|
|
345
|
-
* leaves the list as it was unless `max` changed while it was away.
|
|
346
|
-
*/
|
|
331
|
+
#arrivals = /* @__PURE__ */ new Set();
|
|
347
332
|
#appliedMax = null;
|
|
348
|
-
/**
|
|
349
|
-
* Stimulus reports every toast the list already holds before this runs, and each is
|
|
350
|
-
* taken on then with its holds, meeting no limit. The limit is applied here once, over
|
|
351
|
-
* all of them, unless the list was already held to the current `max`.
|
|
352
|
-
*/
|
|
353
333
|
connect() {
|
|
354
334
|
this.#connected = true;
|
|
355
|
-
this
|
|
335
|
+
const current = this.itemTargets;
|
|
336
|
+
for (const item of this.#arrivals) {
|
|
337
|
+
if (!current.includes(item)) this.#arrivals.delete(item);
|
|
338
|
+
}
|
|
339
|
+
this.#reapply.observe(this.element);
|
|
356
340
|
this.#connectDelegatedEvents();
|
|
357
341
|
for (const item of this.itemTargets) {
|
|
358
342
|
if (!this.#dismiss.tracks(item) && item.dataset.state !== "leaving") {
|
|
359
343
|
this.#startTimer(item);
|
|
360
344
|
}
|
|
361
345
|
}
|
|
362
|
-
if (!Object.is(this.#appliedMax, this
|
|
363
|
-
this.#appliedMax = this
|
|
346
|
+
if (!Object.is(this.#appliedMax, this.#safeMax)) {
|
|
347
|
+
this.#appliedMax = this.#safeMax;
|
|
364
348
|
this.#enforceMax();
|
|
365
349
|
}
|
|
366
350
|
}
|
|
@@ -370,7 +354,7 @@ var ToastController = class extends Controller {
|
|
|
370
354
|
this.#timers.clearAll();
|
|
371
355
|
this.#dismiss.clearAll();
|
|
372
356
|
this.#taken.clear();
|
|
373
|
-
this.#reapply.
|
|
357
|
+
this.#reapply.disconnect();
|
|
374
358
|
this.#spared = [];
|
|
375
359
|
this.#pointer.dispose();
|
|
376
360
|
this.#awaitingPointer.clear();
|
|
@@ -379,66 +363,44 @@ var ToastController = class extends Controller {
|
|
|
379
363
|
}
|
|
380
364
|
this.#rafHandles.clear();
|
|
381
365
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (this.#delegatedList !== element) this.#connectDelegatedEvents(element);
|
|
366
|
+
listTargetConnected() {
|
|
367
|
+
if (this.#delegatedList !== this.listTarget) this.#connectDelegatedEvents();
|
|
385
368
|
}
|
|
386
|
-
/** Releases delegation only when the removed target is its current owner. */
|
|
387
369
|
listTargetDisconnected(element) {
|
|
388
370
|
if (this.#delegatedList === element) this.#disconnectDelegatedEvents();
|
|
389
|
-
|
|
390
|
-
|
|
371
|
+
if (!this.#connected || !this.hasListTarget || this.listTarget === element) return;
|
|
372
|
+
if (this.#delegatedList !== this.listTarget) this.#connectDelegatedEvents();
|
|
391
373
|
for (const item of this.itemTargets) {
|
|
392
|
-
if (item
|
|
393
|
-
if (this.durationValue <= 0) {
|
|
394
|
-
this.#dismiss.disarm(item);
|
|
395
|
-
item.removeAttribute("data-paused");
|
|
396
|
-
} else {
|
|
397
|
-
this.#startTimer(item);
|
|
398
|
-
}
|
|
374
|
+
if (this.#outside.has(item)) this.#takeIn(item);
|
|
399
375
|
}
|
|
400
376
|
}
|
|
401
|
-
/**
|
|
402
|
-
* Holds the list to a changed `max`. Stimulus can also call this ahead of `connect()` —
|
|
403
|
-
* for an attribute that changed while the controller was away, and on every connect
|
|
404
|
-
* for an undeclared `max`, with its default; `connect()` decides then.
|
|
405
|
-
*/
|
|
406
377
|
maxValueChanged() {
|
|
407
378
|
if (!this.#connected) return;
|
|
408
|
-
this.#appliedMax = this
|
|
379
|
+
this.#appliedMax = this.#safeMax;
|
|
409
380
|
this.enforceMaxLimit();
|
|
410
381
|
}
|
|
411
|
-
/**
|
|
412
|
-
* Stimulus lifecycle callback triggered automatically when a new item target
|
|
413
|
-
* enters the DOM, from a client-side injection or a Turbo Stream append alike, and for
|
|
414
|
-
* every toast the list already holds ahead of `connect()`. An item already leaving,
|
|
415
|
-
* parented outside `list`, or already taken on (a move within the list) is skipped; any
|
|
416
|
-
* other is taken on with the holds it already has. On a connected controller the `max`
|
|
417
|
-
* limit is then applied, never to the arrival itself; the toasts reported ahead of
|
|
418
|
-
* `connect()` are left to it.
|
|
419
|
-
*/
|
|
420
382
|
itemTargetConnected(element) {
|
|
421
|
-
if (this.#
|
|
422
|
-
|
|
383
|
+
if (this.#takeIn(element) && this.#connected) this.#enforceMax([element]);
|
|
384
|
+
}
|
|
385
|
+
#takeIn(element) {
|
|
386
|
+
if (this.#taken.has(element) || element.dataset.state === "leaving") return false;
|
|
387
|
+
if (element.parentNode !== this.listTarget) {
|
|
388
|
+
this.#outside.add(element);
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
423
391
|
this.#taken.add(element);
|
|
392
|
+
this.#arrivals.add(element);
|
|
424
393
|
this.#readHolds(element);
|
|
425
394
|
this.#startTimer(element);
|
|
426
|
-
element
|
|
427
|
-
this.#cancelAnimation(element);
|
|
395
|
+
this.#setPhase(element, "entering");
|
|
428
396
|
const handle = window.requestAnimationFrame(() => {
|
|
429
397
|
this.#rafHandles.delete(element);
|
|
430
398
|
if (element.parentNode !== this.listTarget || element.dataset.state === "leaving") return;
|
|
431
|
-
element
|
|
399
|
+
this.#setPhase(element, "visible");
|
|
432
400
|
});
|
|
433
401
|
this.#rafHandles.set(element, handle);
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Lets go of a toast that left the list or stopped being an item: its timer, holds,
|
|
438
|
-
* `data-paused` and pending frame. After a move within the list the node is already
|
|
439
|
-
* back, still an item, when Stimulus reports it, so it keeps all of those, and only the
|
|
440
|
-
* holds the move may have ended are read again.
|
|
441
|
-
*/
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
442
404
|
itemTargetDisconnected(element) {
|
|
443
405
|
const moved = this.#isShown(element) && element.matches(targetSelector(this.identifier, "item"));
|
|
444
406
|
if (moved) {
|
|
@@ -446,26 +408,12 @@ var ToastController = class extends Controller {
|
|
|
446
408
|
return;
|
|
447
409
|
}
|
|
448
410
|
this.#taken.delete(element);
|
|
411
|
+
this.#phases.delete(element);
|
|
412
|
+
if (this.#connected) this.#arrivals.delete(element);
|
|
449
413
|
element.removeAttribute("data-paused");
|
|
450
414
|
this.#letGo(element);
|
|
451
415
|
this.#cancelAnimation(element);
|
|
452
416
|
}
|
|
453
|
-
/**
|
|
454
|
-
* Shows a new toast. Accepts its content from either a Stimulus action param
|
|
455
|
-
* (attribute-only trigger) or a programmatic `show` CustomEvent `detail`
|
|
456
|
-
* (remote / Turbo trigger); the action param wins when both are present.
|
|
457
|
-
*
|
|
458
|
-
* <button data-action="click->stimeo--toast#show"
|
|
459
|
-
* data-stimeo--toast-message-param="Saved"
|
|
460
|
-
* data-stimeo--toast-type-param="status">Show</button>
|
|
461
|
-
*
|
|
462
|
-
* element.dispatchEvent(new CustomEvent("show", { detail: { message, type } }))
|
|
463
|
-
*
|
|
464
|
-
* Clones the template slot, interpolates the message, and appends to the list.
|
|
465
|
-
* `message` is the wording key the rest of the library dispatches under, so a
|
|
466
|
-
* part that already carries its own wording — `stimeo--clipboard:copy`,
|
|
467
|
-
* `stimeo--auto-submit:done` — wires straight into this action.
|
|
468
|
-
*/
|
|
469
417
|
show(event) {
|
|
470
418
|
if (!this.hasTemplateTarget || !this.hasListTarget) return;
|
|
471
419
|
const message = this.#readField(event, "message");
|
|
@@ -484,11 +432,6 @@ var ToastController = class extends Controller {
|
|
|
484
432
|
this.listTarget.appendChild(item);
|
|
485
433
|
this.dispatch("show", { detail: { item } });
|
|
486
434
|
}
|
|
487
|
-
/**
|
|
488
|
-
* Reads a string field from a Stimulus action param or a CustomEvent `detail`,
|
|
489
|
-
* preferring the action param. Returns null unless a non-empty string is found,
|
|
490
|
-
* so untrusted runtime payloads cannot inject non-string values.
|
|
491
|
-
*/
|
|
492
435
|
#readField(event, key) {
|
|
493
436
|
const params = event.params;
|
|
494
437
|
const fromParams = params?.[key];
|
|
@@ -500,97 +443,77 @@ var ToastController = class extends Controller {
|
|
|
500
443
|
}
|
|
501
444
|
return null;
|
|
502
445
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const item = this.#
|
|
446
|
+
dismiss(input) {
|
|
447
|
+
const source = actionSource(input);
|
|
448
|
+
const item = this.#itemFromSource(source);
|
|
506
449
|
if (!item) return;
|
|
507
|
-
this.#removeWithTransition(item,
|
|
450
|
+
this.#removeWithTransition(item, source.reason);
|
|
508
451
|
}
|
|
509
|
-
/** Dismisses the focused toast when Escape is pressed. */
|
|
510
452
|
onKeydown(event) {
|
|
511
453
|
if (event.key === "Escape") {
|
|
512
454
|
if (event.defaultPrevented || event.isComposing) return;
|
|
513
|
-
const item = this.#
|
|
455
|
+
const item = this.#itemFromSource(actionSource(event));
|
|
514
456
|
if (!item) return;
|
|
515
457
|
event.preventDefault();
|
|
516
458
|
this.#removeWithTransition(item, "user");
|
|
517
459
|
}
|
|
518
460
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
* snapshotted on the first active reason, and only resumed once *every*
|
|
524
|
-
* reason has been released (see {@link resume}). This keeps a toast paused
|
|
525
|
-
* while it is still hovered *or* focused, per WCAG 2.2.1. The hold is recorded
|
|
526
|
-
* whether or not a timer runs, so the `max` limit passes over the toast either
|
|
527
|
-
* way; `data-paused` marks a timer that stopped, and only that.
|
|
528
|
-
*/
|
|
529
|
-
pause(event) {
|
|
530
|
-
const item = this.#itemFromEvent(event);
|
|
531
|
-
if (item && this.#dismiss.pause(item, this.#pauseReason(event))) {
|
|
461
|
+
pause(input) {
|
|
462
|
+
const source = actionSource(input);
|
|
463
|
+
const item = this.#itemFromSource(source);
|
|
464
|
+
if (item && this.#dismiss.pause(item, this.#pauseReason(source.event))) {
|
|
532
465
|
item.setAttribute("data-paused", "true");
|
|
533
466
|
}
|
|
534
467
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
* moving into: its own hold only arrives after this event.
|
|
539
|
-
*/
|
|
540
|
-
resume(event) {
|
|
541
|
-
const item = this.#itemFromEvent(event);
|
|
468
|
+
resume(input) {
|
|
469
|
+
const source = actionSource(input);
|
|
470
|
+
const item = this.#itemFromSource(source);
|
|
542
471
|
if (item) {
|
|
543
472
|
this.#release(
|
|
544
473
|
item,
|
|
545
|
-
this.#pauseReason(event),
|
|
546
|
-
ownerOf(this.itemTargets, event
|
|
474
|
+
this.#pauseReason(source.event),
|
|
475
|
+
ownerOf(this.itemTargets, source.event?.relatedTarget)
|
|
547
476
|
);
|
|
548
477
|
}
|
|
549
478
|
}
|
|
550
|
-
/** Releases one reason on `item`; releasing the last one applies the limit again. */
|
|
551
479
|
#release(item, reason, spare) {
|
|
552
480
|
const held = this.#dismiss.isHeld(item);
|
|
553
481
|
if (this.#dismiss.resume(item, reason)) item.removeAttribute("data-paused");
|
|
554
482
|
if (held && !this.#dismiss.isHeld(item)) this.#limitLater(spare);
|
|
555
483
|
}
|
|
556
|
-
/**
|
|
557
|
-
* Applies the limit again once the current event is over, sparing `spare`. A hold can
|
|
558
|
-
* end in a `focusout` the engine fires while it is itself taking the node out for a
|
|
559
|
-
* script's move or removal, and taking the toast away inside that event would pull
|
|
560
|
-
* the node from under the caller's own call.
|
|
561
|
-
*/
|
|
562
484
|
#limitLater(spare) {
|
|
563
485
|
if (spare) this.#spared.push(spare);
|
|
564
486
|
this.#reapply.schedule();
|
|
565
487
|
}
|
|
566
|
-
|
|
488
|
+
#setPhase(item, phase) {
|
|
489
|
+
this.#phases.set(item, phase);
|
|
490
|
+
item.setAttribute("data-state", phase);
|
|
491
|
+
}
|
|
567
492
|
#reapplyLimit() {
|
|
493
|
+
for (const item of this.itemTargets) {
|
|
494
|
+
if (!this.#taken.has(item) || !this.hasListTarget || item.parentNode !== this.listTarget)
|
|
495
|
+
continue;
|
|
496
|
+
const phase = this.#phases.get(item);
|
|
497
|
+
if (item.dataset.state === "leaving") this.#phases.set(item, "leaving");
|
|
498
|
+
else if (phase) item.setAttribute("data-state", phase);
|
|
499
|
+
if (this.#dismiss.isHeld(item) && this.#dismiss.tracks(item))
|
|
500
|
+
item.setAttribute("data-paused", "true");
|
|
501
|
+
else item.removeAttribute("data-paused");
|
|
502
|
+
}
|
|
568
503
|
const spared = this.#spared;
|
|
569
504
|
this.#spared = [];
|
|
570
505
|
this.#enforceMax(spared);
|
|
571
506
|
}
|
|
572
|
-
/**
|
|
573
|
-
* Reads the holds of a toast that moved within the list. Focus is read at once: an
|
|
574
|
-
* ordinary move ends it with a `focusout` and `moveBefore` keeps it. Where the pointer
|
|
575
|
-
* is, nothing says until it moves again: no `mouseout` reaches a moved node, and
|
|
576
|
-
* `:hover` can answer from before the move. A toast still held waits for that movement.
|
|
577
|
-
*/
|
|
578
507
|
#afterMove(item) {
|
|
579
508
|
if (!item.contains(document.activeElement)) this.#release(item, "focus", null);
|
|
580
509
|
if (this.#dismiss.isHeld(item)) this.#awaitPointer(item);
|
|
581
510
|
}
|
|
582
|
-
/**
|
|
583
|
-
* Takes the holds a toast already has as it is taken on: focus inside it, and the pointer
|
|
584
|
-
* over it by `:hover`. That answer can date from before the toast came here, so a hover
|
|
585
|
-
* hold taken on it waits for the next pointer movement to be confirmed.
|
|
586
|
-
*/
|
|
587
511
|
#readHolds(item) {
|
|
588
512
|
if (item.contains(document.activeElement)) this.#dismiss.pause(item, "focus");
|
|
589
513
|
if (!item.matches(":hover")) return;
|
|
590
514
|
this.#dismiss.pause(item, "hover");
|
|
591
515
|
this.#awaitPointer(item);
|
|
592
516
|
}
|
|
593
|
-
/** Leaves `item`'s hover hold to be confirmed or let go on the next pointer movement. */
|
|
594
517
|
#awaitPointer(item) {
|
|
595
518
|
this.#awaitingPointer.add(item);
|
|
596
519
|
this.#pointer.add(document, "pointermove", this.#onPointerMove, {
|
|
@@ -598,10 +521,6 @@ var ToastController = class extends Controller {
|
|
|
598
521
|
passive: true
|
|
599
522
|
});
|
|
600
523
|
}
|
|
601
|
-
/**
|
|
602
|
-
* Reads `:hover` once the pointer has moved, when the answer is current, and lets go of
|
|
603
|
-
* the hover hold of every waiting toast the pointer is not over. Listens once.
|
|
604
|
-
*/
|
|
605
524
|
#onPointerMove = () => {
|
|
606
525
|
this.#pointer.dispose();
|
|
607
526
|
const waiting = [...this.#awaitingPointer];
|
|
@@ -610,43 +529,30 @@ var ToastController = class extends Controller {
|
|
|
610
529
|
if (!item.matches(":hover")) this.#release(item, "hover", null);
|
|
611
530
|
}
|
|
612
531
|
};
|
|
613
|
-
/**
|
|
614
|
-
* Drops a toast's timer and holds as it leaves, and stops waiting on the pointer for it.
|
|
615
|
-
* A toast something still held counts as released: the limit is applied again.
|
|
616
|
-
*/
|
|
617
532
|
#letGo(item) {
|
|
618
533
|
if (this.#dismiss.isHeld(item)) this.#limitLater(null);
|
|
619
534
|
this.#dismiss.clear(item);
|
|
620
535
|
this.#awaitingPointer.delete(item);
|
|
621
536
|
if (this.#awaitingPointer.size === 0) this.#pointer.dispose();
|
|
622
537
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if (
|
|
538
|
+
#itemFromSource(source) {
|
|
539
|
+
const target = source.origin ?? source.host;
|
|
540
|
+
if (!target || !this.hasListTarget) return null;
|
|
541
|
+
if (target.closest(`[data-controller~="${this.identifier}"]`) !== this.element) return null;
|
|
627
542
|
const item = target.closest(targetSelector(this.identifier, "item"));
|
|
628
|
-
return item && this.listTarget.contains(item) ? item : null;
|
|
543
|
+
return item && this.itemTargets.includes(item) && this.listTarget.contains(item) ? item : null;
|
|
629
544
|
}
|
|
630
|
-
/** Classifies a pause/resume event as a hover or focus reason. */
|
|
631
545
|
#pauseReason(event) {
|
|
632
|
-
return event
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* Arms a toast's auto-dismiss; a non-positive `duration` means it never expires.
|
|
636
|
-
* On a toast hover or focus already holds, the timer waits for the release.
|
|
637
|
-
* `data-paused` is written from that state, so a mark the toast carried in with it,
|
|
638
|
-
* from a restored page, does not outlive it.
|
|
639
|
-
*
|
|
640
|
-
* @stimeoRuntimeOnly `duration` is the delay of the one dismissal timer this call arms.
|
|
641
|
-
*/
|
|
546
|
+
return event?.type === "focusin" || event?.type === "focusout" ? "focus" : "hover";
|
|
547
|
+
}
|
|
642
548
|
#startTimer(element) {
|
|
643
549
|
if (element.dataset.state === "leaving" || element.parentNode !== this.listTarget) return;
|
|
644
|
-
const armed = this
|
|
550
|
+
const armed = this.#safeDuration > 0;
|
|
645
551
|
if (armed) {
|
|
646
552
|
this.#dismiss.set(
|
|
647
553
|
element,
|
|
648
554
|
() => this.#removeWithTransition(element, "timeout"),
|
|
649
|
-
this
|
|
555
|
+
this.#safeDuration
|
|
650
556
|
);
|
|
651
557
|
}
|
|
652
558
|
if (armed && this.#dismiss.isHeld(element)) element.setAttribute("data-paused", "true");
|
|
@@ -656,7 +562,7 @@ var ToastController = class extends Controller {
|
|
|
656
562
|
if (element.dataset.state === "leaving" || element.parentNode !== this.listTarget) return;
|
|
657
563
|
this.#letGo(element);
|
|
658
564
|
this.#cancelAnimation(element);
|
|
659
|
-
element
|
|
565
|
+
this.#setPhase(element, "leaving");
|
|
660
566
|
const finalize = () => {
|
|
661
567
|
if (element.parentNode !== this.listTarget) return;
|
|
662
568
|
this.listTarget.removeChild(element);
|
|
@@ -669,48 +575,24 @@ var ToastController = class extends Controller {
|
|
|
669
575
|
finalize();
|
|
670
576
|
}
|
|
671
577
|
}
|
|
672
|
-
/**
|
|
673
|
-
* Removes the oldest toasts while the list shows more than `maxValue`, passing
|
|
674
|
-
* over every toast hover or focus holds.
|
|
675
|
-
*
|
|
676
|
-
* Public (not `#private`) as a deterministic seam: enforcement normally runs
|
|
677
|
-
* as each toast is taken on in `itemTargetConnected`, a Stimulus callback delivered
|
|
678
|
-
* through a MutationObserver, which a DOM-only environment does not reliably fire —
|
|
679
|
-
* so it can also be invoked directly. It counts only the toasts taken on. It is not a
|
|
680
|
-
* user-wired action.
|
|
681
|
-
*/
|
|
682
578
|
enforceMaxLimit() {
|
|
683
579
|
this.#enforceMax();
|
|
684
580
|
}
|
|
685
|
-
/**
|
|
686
|
-
* Removes the oldest toasts with reason `limit` while the list shows more than `max`
|
|
687
|
-
* (`0` or less, or not a finite number, means no limit), passing over every toast hover
|
|
688
|
-
* or focus holds and those in `spared` — the toast just taken on, or the ones the
|
|
689
|
-
* pointer or focus moved into. Only toasts taken on count or go, so one Stimulus has
|
|
690
|
-
* yet to report is neither; a toast already leaving does not count. With nothing else
|
|
691
|
-
* left to go the list stays over the limit. The count is read again on every step,
|
|
692
|
-
* because a `dismiss` listener may already have changed it.
|
|
693
|
-
*
|
|
694
|
-
* @stimeoRenderRoot
|
|
695
|
-
*/
|
|
696
581
|
#enforceMax(spared = []) {
|
|
697
|
-
if (
|
|
698
|
-
for (const item of this
|
|
699
|
-
if (this.#shownCount() <= this
|
|
582
|
+
if (this.#safeMax <= 0) return;
|
|
583
|
+
for (const item of [...this.#arrivals]) {
|
|
584
|
+
if (this.#shownCount() <= this.#safeMax) return;
|
|
700
585
|
if (this.#isShown(item) && !spared.includes(item) && !this.#dismiss.isHeld(item)) {
|
|
701
586
|
this.#removeWithTransition(item, "limit");
|
|
702
587
|
}
|
|
703
588
|
}
|
|
704
589
|
}
|
|
705
|
-
/** How many toasts the list shows: the ones taken on that are still shown. */
|
|
706
590
|
#shownCount() {
|
|
707
591
|
return this.itemTargets.filter((item) => this.#isShown(item)).length;
|
|
708
592
|
}
|
|
709
|
-
/** Whether the list shows `item`: a toast taken on, a child of `list`, not leaving. */
|
|
710
593
|
#isShown(item) {
|
|
711
594
|
return this.#taken.has(item) && this.hasListTarget && item.parentNode === this.listTarget && item.dataset.state !== "leaving";
|
|
712
595
|
}
|
|
713
|
-
/** Wires stable-container delegation so newly appended items work immediately. */
|
|
714
596
|
#connectDelegatedEvents(list = this.hasListTarget ? this.listTarget : null) {
|
|
715
597
|
this.#disconnectDelegatedEvents();
|
|
716
598
|
if (!list) return;
|
|
@@ -719,7 +601,6 @@ var ToastController = class extends Controller {
|
|
|
719
601
|
this.#delegatedList.addEventListener(type, this.#onListEvent);
|
|
720
602
|
}
|
|
721
603
|
}
|
|
722
|
-
/** Releases every delegated listener from the exact list that owns it. */
|
|
723
604
|
#disconnectDelegatedEvents() {
|
|
724
605
|
if (!this.#delegatedList) return;
|
|
725
606
|
for (const type of DELEGATED_EVENTS) {
|
|
@@ -727,7 +608,6 @@ var ToastController = class extends Controller {
|
|
|
727
608
|
}
|
|
728
609
|
this.#delegatedList = null;
|
|
729
610
|
}
|
|
730
|
-
/** Routes every delegated item interaction from the stable list. */
|
|
731
611
|
#onListEvent = (event) => {
|
|
732
612
|
if (event.type === "click") {
|
|
733
613
|
const target = event.target instanceof Element ? event.target : null;
|
|
@@ -740,22 +620,31 @@ var ToastController = class extends Controller {
|
|
|
740
620
|
else this.resume(event);
|
|
741
621
|
}
|
|
742
622
|
};
|
|
743
|
-
/** Whether a bubbling focus/pointer event enters or leaves a toast boundary. */
|
|
744
623
|
#crossesItemBoundary(event) {
|
|
745
|
-
const item = this.#
|
|
624
|
+
const item = this.#itemFromSource(actionSource(event));
|
|
746
625
|
if (!item) return false;
|
|
747
626
|
const related = "relatedTarget" in event ? event.relatedTarget : null;
|
|
748
627
|
return !(related instanceof Node && item.contains(related));
|
|
749
628
|
}
|
|
750
|
-
/** Cancels the pending entering-to-visible frame owned by one item. */
|
|
751
629
|
#cancelAnimation(element) {
|
|
752
630
|
const handle = this.#rafHandles.get(element);
|
|
753
631
|
if (handle === void 0) return;
|
|
754
632
|
window.cancelAnimationFrame(handle);
|
|
755
633
|
this.#rafHandles.delete(element);
|
|
756
634
|
}
|
|
635
|
+
get #safeDuration() {
|
|
636
|
+
return this.#numbers.read(
|
|
637
|
+
this,
|
|
638
|
+
"duration",
|
|
639
|
+
this.durationValue,
|
|
640
|
+
_ToastController.values.duration.default,
|
|
641
|
+
_ToastController.valueConstraints.duration
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
get #safeMax() {
|
|
645
|
+
return this.#numbers.read(this, "max", this.maxValue, 0, _ToastController.valueConstraints.max);
|
|
646
|
+
}
|
|
757
647
|
};
|
|
758
648
|
|
|
759
649
|
export { ToastController };
|
|
760
|
-
//# sourceMappingURL=toast_controller.js.map
|
|
761
650
|
//# sourceMappingURL=toast_controller.js.map
|