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/announce.ts
|
|
6
25
|
function announce(message, options = {}) {
|
|
7
26
|
const text = message.trim();
|
|
8
27
|
if (text.length === 0) return;
|
|
@@ -13,36 +32,6 @@ function announce(message, options = {}) {
|
|
|
13
32
|
);
|
|
14
33
|
}
|
|
15
34
|
|
|
16
|
-
// src/utils/before_cache_reset.ts
|
|
17
|
-
var BeforeCacheReset = class _BeforeCacheReset {
|
|
18
|
-
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
19
|
-
static #subscribers = /* @__PURE__ */ new Set();
|
|
20
|
-
/** The shared listener; installed while at least one instance is subscribed. */
|
|
21
|
-
static #onBeforeCache = () => {
|
|
22
|
-
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
23
|
-
};
|
|
24
|
-
#rewind;
|
|
25
|
-
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
26
|
-
constructor(rewind) {
|
|
27
|
-
this.#rewind = rewind;
|
|
28
|
-
}
|
|
29
|
-
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
30
|
-
activate() {
|
|
31
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
32
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
33
|
-
if (first) {
|
|
34
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
38
|
-
deactivate() {
|
|
39
|
-
_BeforeCacheReset.#subscribers.delete(this);
|
|
40
|
-
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
41
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// src/utils/event_owner.ts
|
|
46
35
|
function ownerIndex(candidates, node) {
|
|
47
36
|
if (!(node instanceof Node)) return -1;
|
|
48
37
|
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
@@ -51,43 +40,26 @@ function ownerOf(candidates, node) {
|
|
|
51
40
|
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
52
41
|
}
|
|
53
42
|
|
|
54
|
-
|
|
43
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
44
|
+
|
|
55
45
|
var TimerRegistry = class {
|
|
56
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
57
46
|
ids = /* @__PURE__ */ new Set();
|
|
58
|
-
/**
|
|
59
|
-
* Cancels a single tracked timer.
|
|
60
|
-
*
|
|
61
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
62
|
-
* registry), so callers can clear defensively without guarding.
|
|
63
|
-
*/
|
|
64
47
|
clear(id) {
|
|
65
48
|
if (this.ids.delete(id)) {
|
|
66
49
|
this.cancel(id);
|
|
67
50
|
}
|
|
68
51
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
71
|
-
* to guarantee no timer outlives the element.
|
|
72
|
-
*/
|
|
73
52
|
clearAll() {
|
|
74
53
|
for (const id of this.ids) {
|
|
75
54
|
this.cancel(id);
|
|
76
55
|
}
|
|
77
56
|
this.ids.clear();
|
|
78
57
|
}
|
|
79
|
-
/** Number of timers currently tracked (pending). */
|
|
80
58
|
get size() {
|
|
81
59
|
return this.ids.size;
|
|
82
60
|
}
|
|
83
61
|
};
|
|
84
62
|
var SafeTimeout = class extends TimerRegistry {
|
|
85
|
-
/**
|
|
86
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
87
|
-
*
|
|
88
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
89
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
90
|
-
*/
|
|
91
63
|
set(callback, delay) {
|
|
92
64
|
const id = this.schedule(() => {
|
|
93
65
|
this.ids.delete(id);
|
|
@@ -104,16 +76,9 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
104
76
|
}
|
|
105
77
|
};
|
|
106
78
|
|
|
107
|
-
// src/utils/keyed_timers.ts
|
|
108
79
|
var KeyedTimers = class {
|
|
109
80
|
#timers = new SafeTimeout();
|
|
110
|
-
/** The pending timer of each key; an entry lives exactly as long as its timer. */
|
|
111
81
|
#pending = /* @__PURE__ */ new Map();
|
|
112
|
-
/**
|
|
113
|
-
* Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had
|
|
114
|
-
* pending. The entry is dropped before the callback runs, so the callback sees
|
|
115
|
-
* the key unarmed and may arm it again for the next round.
|
|
116
|
-
*/
|
|
117
82
|
set(key, callback, delay) {
|
|
118
83
|
this.clear(key);
|
|
119
84
|
const id = this.#timers.set(() => {
|
|
@@ -122,76 +87,46 @@ var KeyedTimers = class {
|
|
|
122
87
|
}, delay);
|
|
123
88
|
this.#pending.set(key, id);
|
|
124
89
|
}
|
|
125
|
-
/**
|
|
126
|
-
* Cancels `key`'s pending timer, if it has one.
|
|
127
|
-
*
|
|
128
|
-
* A timer id is a positive integer, so `-1` stands for "nothing pending" and
|
|
129
|
-
* the registry ignores an id it does not own — the unarmed case needs no
|
|
130
|
-
* branch of its own, and no other key's timer can be reached from here.
|
|
131
|
-
*/
|
|
132
90
|
clear(key) {
|
|
133
91
|
this.#timers.clear(this.#pending.get(key) ?? -1);
|
|
134
92
|
this.#pending.delete(key);
|
|
135
93
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Cancels every pending timer and forgets every key. Call this from a
|
|
138
|
-
* controller's `disconnect()` so no timer, and no entry, outlives the element.
|
|
139
|
-
*/
|
|
140
94
|
clearAll() {
|
|
141
95
|
this.#timers.clearAll();
|
|
142
96
|
this.#pending.clear();
|
|
143
97
|
}
|
|
144
|
-
/** Whether `key` has a timer pending. */
|
|
145
98
|
has(key) {
|
|
146
99
|
return this.#pending.has(key);
|
|
147
100
|
}
|
|
148
101
|
};
|
|
149
102
|
|
|
150
|
-
// src/utils/listener_set.ts
|
|
151
103
|
var ListenerSet = class {
|
|
152
|
-
/** The generation every `add` joins until the next `dispose()`. */
|
|
153
104
|
#abort = new AbortController();
|
|
154
|
-
/**
|
|
155
|
-
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
156
|
-
*
|
|
157
|
-
* The set supplies the signal, so `options` carries everything else the DOM
|
|
158
|
-
* accepts — `capture` included, which has to match at release time and no
|
|
159
|
-
* longer has a second place to drift from.
|
|
160
|
-
*/
|
|
161
105
|
add(target, type, handler, options) {
|
|
162
106
|
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
163
107
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Releases every listener of the open generation, synchronously, and opens the
|
|
166
|
-
* next one. Idempotent, and safe before anything has been added.
|
|
167
|
-
*/
|
|
168
108
|
dispose() {
|
|
169
109
|
this.#abort.abort();
|
|
170
110
|
this.#abort = new AbortController();
|
|
171
111
|
}
|
|
172
112
|
};
|
|
173
113
|
|
|
174
|
-
// src/utils/microtask_coalescer.ts
|
|
175
114
|
var MicrotaskCoalescer = class {
|
|
176
115
|
#run;
|
|
177
116
|
#queued = false;
|
|
178
117
|
#active = false;
|
|
179
118
|
#generation = 0;
|
|
180
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
181
119
|
constructor(run) {
|
|
182
120
|
this.#run = run;
|
|
183
121
|
}
|
|
184
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
185
122
|
activate() {
|
|
186
123
|
this.#active = true;
|
|
187
124
|
}
|
|
188
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
189
125
|
cancel() {
|
|
190
126
|
this.#active = false;
|
|
191
127
|
this.#queued = false;
|
|
192
128
|
this.#generation += 1;
|
|
193
129
|
}
|
|
194
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
195
130
|
schedule() {
|
|
196
131
|
if (!this.#active || this.#queued) return;
|
|
197
132
|
this.#queued = true;
|
|
@@ -204,27 +139,85 @@ var MicrotaskCoalescer = class {
|
|
|
204
139
|
}
|
|
205
140
|
};
|
|
206
141
|
|
|
207
|
-
|
|
142
|
+
var MorphRenderWatcher = class {
|
|
143
|
+
#listeners = new ListenerSet();
|
|
144
|
+
#pass;
|
|
145
|
+
#element = null;
|
|
146
|
+
#onMorph = () => this.#pass.schedule();
|
|
147
|
+
constructor(run) {
|
|
148
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
149
|
+
}
|
|
150
|
+
observe(element) {
|
|
151
|
+
if (this.#element === element) return;
|
|
152
|
+
this.disconnect();
|
|
153
|
+
this.#element = element;
|
|
154
|
+
this.#pass.activate();
|
|
155
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
156
|
+
}
|
|
157
|
+
schedule() {
|
|
158
|
+
this.#pass.schedule();
|
|
159
|
+
}
|
|
160
|
+
disconnect() {
|
|
161
|
+
this.#listeners.dispose();
|
|
162
|
+
this.#pass.cancel();
|
|
163
|
+
this.#element = null;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
var NUMBER_BOUNDS = {
|
|
168
|
+
finite: { finite: true },
|
|
169
|
+
timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
|
|
170
|
+
function matchesNumberBounds(value, bounds) {
|
|
171
|
+
if (!Number.isFinite(value)) {
|
|
172
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
173
|
+
if (direction === null) return false;
|
|
174
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
175
|
+
}
|
|
176
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
177
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
178
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
179
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
180
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function readNumber(raw, fallback, bounds) {
|
|
185
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
var NumberValueReader = class {
|
|
189
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
190
|
+
read(owner, name, raw, fallback, bounds) {
|
|
191
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
192
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
193
|
+
this.#lastRejected.delete(name);
|
|
194
|
+
return resolved;
|
|
195
|
+
}
|
|
196
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
197
|
+
const literal = owner.element.getAttribute(attribute);
|
|
198
|
+
if (literal === null) {
|
|
199
|
+
this.#lastRejected.delete(name);
|
|
200
|
+
return resolved;
|
|
201
|
+
}
|
|
202
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
203
|
+
this.#lastRejected.set(name, literal);
|
|
204
|
+
console.warn(
|
|
205
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return resolved;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
208
212
|
var PausableTimers = class {
|
|
209
213
|
#timers = new KeyedTimers();
|
|
210
|
-
/** Every key armed or held; an entry goes as its timer fires, or with the key. */
|
|
211
214
|
#entries = /* @__PURE__ */ new Map();
|
|
212
|
-
/**
|
|
213
|
-
* Arms `callback` after `delay` ms for `key`, replacing whatever `key` had.
|
|
214
|
-
* A held key stays held and banks `delay` for its resume.
|
|
215
|
-
*/
|
|
216
215
|
set(key, callback, delay) {
|
|
217
216
|
const reasons = this.#entries.get(key)?.reasons ?? /* @__PURE__ */ new Set();
|
|
218
217
|
const entry = { callback, startedAt: 0, remaining: delay, reasons };
|
|
219
218
|
this.#entries.set(key, entry);
|
|
220
219
|
if (reasons.size === 0) this.#arm(key, entry, callback);
|
|
221
220
|
}
|
|
222
|
-
/**
|
|
223
|
-
* Holds `key` for `reason` and banks the time left, with a floor of one
|
|
224
|
-
* millisecond. A key with no timer is held all the same. Reports whether this
|
|
225
|
-
* call is the one that stopped a running timer, so the caller can write its
|
|
226
|
-
* held-state hook exactly once.
|
|
227
|
-
*/
|
|
228
221
|
pause(key, reason) {
|
|
229
222
|
let entry = this.#entries.get(key);
|
|
230
223
|
if (entry === void 0) {
|
|
@@ -237,11 +230,6 @@ var PausableTimers = class {
|
|
|
237
230
|
entry.remaining = Math.max(1, entry.remaining - (Date.now() - entry.startedAt));
|
|
238
231
|
return true;
|
|
239
232
|
}
|
|
240
|
-
/**
|
|
241
|
-
* Releases `reason` on `key`, arming the banked time again once no reason is
|
|
242
|
-
* left; a key released with no timer is forgotten. Reports whether this call
|
|
243
|
-
* is the one that started the timer again.
|
|
244
|
-
*/
|
|
245
233
|
resume(key, reason) {
|
|
246
234
|
const entry = this.#entries.get(key);
|
|
247
235
|
if (entry === void 0) return false;
|
|
@@ -255,38 +243,20 @@ var PausableTimers = class {
|
|
|
255
243
|
this.#arm(key, entry, callback);
|
|
256
244
|
return true;
|
|
257
245
|
}
|
|
258
|
-
/** Whether any reason holds `key`, with or without a timer. */
|
|
259
246
|
isHeld(key) {
|
|
260
247
|
return (this.#entries.get(key)?.reasons.size ?? 0) > 0;
|
|
261
248
|
}
|
|
262
|
-
/** Whether `key` is armed or held — that is, whether this registry drives it at all. */
|
|
263
249
|
tracks(key) {
|
|
264
250
|
return this.#entries.has(key);
|
|
265
251
|
}
|
|
266
|
-
/**
|
|
267
|
-
* Cancels `key`'s timer and keeps its hold, so a release arms nothing; a key
|
|
268
|
-
* nothing holds is forgotten.
|
|
269
|
-
*/
|
|
270
|
-
disarm(key) {
|
|
271
|
-
this.#timers.clear(key);
|
|
272
|
-
const entry = this.#entries.get(key);
|
|
273
|
-
if (entry?.reasons.size) entry.callback = null;
|
|
274
|
-
else this.#entries.delete(key);
|
|
275
|
-
}
|
|
276
|
-
/** Cancels `key`'s timer and drops its hold. */
|
|
277
252
|
clear(key) {
|
|
278
253
|
this.#timers.clear(key);
|
|
279
254
|
this.#entries.delete(key);
|
|
280
255
|
}
|
|
281
|
-
/**
|
|
282
|
-
* Cancels every timer and forgets every key. Call this from a controller's
|
|
283
|
-
* `disconnect()` so neither a timer nor a hold outlives the element.
|
|
284
|
-
*/
|
|
285
256
|
clearAll() {
|
|
286
257
|
this.#timers.clearAll();
|
|
287
258
|
this.#entries.clear();
|
|
288
259
|
}
|
|
289
|
-
/** Starts `entry`'s banked time running for `key`, and drops it as `callback` fires. */
|
|
290
260
|
#arm(key, entry, callback) {
|
|
291
261
|
entry.startedAt = Date.now();
|
|
292
262
|
this.#timers.set(
|
|
@@ -300,12 +270,10 @@ var PausableTimers = class {
|
|
|
300
270
|
}
|
|
301
271
|
};
|
|
302
272
|
|
|
303
|
-
// src/utils/target_selector.ts
|
|
304
273
|
function targetSelector(identifier, name) {
|
|
305
274
|
return `[data-${identifier}-target~="${name}"]`;
|
|
306
275
|
}
|
|
307
276
|
|
|
308
|
-
// src/utils/transition_completion.ts
|
|
309
277
|
function timeMs(value) {
|
|
310
278
|
const trimmed = value.trim();
|
|
311
279
|
const amount = Number.parseFloat(trimmed);
|
|
@@ -339,11 +307,11 @@ function maxTransitionTotalMs(style) {
|
|
|
339
307
|
return maxTotalMs(transitionTimings(style));
|
|
340
308
|
}
|
|
341
309
|
|
|
342
|
-
// src/controllers/flash_controller.ts
|
|
343
310
|
var ASSERTIVE_TYPES = /* @__PURE__ */ new Set(["alert", "error"]);
|
|
344
311
|
var MESSAGE_PART = "message";
|
|
345
|
-
var
|
|
346
|
-
|
|
312
|
+
var SHOWN_ATTRIBUTE = "shown";
|
|
313
|
+
var FlashController = class _FlashController extends Controller {
|
|
314
|
+
#numbers = new NumberValueReader();
|
|
347
315
|
get #messageSelector() {
|
|
348
316
|
return targetSelector(this.identifier, MESSAGE_PART);
|
|
349
317
|
}
|
|
@@ -353,54 +321,29 @@ var FlashController = class extends Controller {
|
|
|
353
321
|
pauseOnHover: { type: Boolean, default: true },
|
|
354
322
|
max: { type: Number, default: 0 }
|
|
355
323
|
};
|
|
324
|
+
static valueConstraints = {
|
|
325
|
+
duration: NUMBER_BOUNDS.timer,
|
|
326
|
+
max: NUMBER_BOUNDS.finite
|
|
327
|
+
};
|
|
356
328
|
static actions = ["dismiss"];
|
|
357
329
|
static events = ["show", "dismiss", "reconcile"];
|
|
358
|
-
/** Removal timers for the leaving transition; the auto-dismiss ones live below. */
|
|
359
330
|
#timers = new SafeTimeout();
|
|
360
|
-
/**
|
|
361
|
-
* Per-message auto-dismiss held open while the message is hovered or focused, and
|
|
362
|
-
* the one record of which messages hover or focus holds, which the cap reads.
|
|
363
|
-
*/
|
|
364
331
|
#dismiss = new PausableTimers();
|
|
365
|
-
/**
|
|
366
|
-
* Auto-dismiss of the messages taken on while `pauseOnHover` is off. It runs on
|
|
367
|
-
* whatever holds the message; the hold itself still keeps the cap away.
|
|
368
|
-
*/
|
|
369
332
|
#fixedDismiss = new KeyedTimers();
|
|
370
|
-
|
|
371
|
-
#reapply = new MicrotaskCoalescer(() => this.#reapplyCap());
|
|
372
|
-
/** Messages the pointer or focus moved into since the cap was last applied again. */
|
|
333
|
+
#reapply = new MorphRenderWatcher(() => this.#reapplyCap());
|
|
373
334
|
#spared = [];
|
|
374
|
-
/** The next pointer movement, listened for while a hover hold waits on it. */
|
|
375
335
|
#pointer = new ListenerSet();
|
|
376
|
-
/** Messages whose hover hold waits for the next pointer movement to be confirmed. */
|
|
377
336
|
#awaitingPointer = /* @__PURE__ */ new Set();
|
|
378
337
|
#observer = null;
|
|
379
|
-
/** Whether the controller is between `connect()` and `disconnect()`. */
|
|
380
338
|
#connected = false;
|
|
381
|
-
|
|
382
|
-
* The `max` the stack was last held to, or `null` before the first time. `connect()`
|
|
383
|
-
* applies the cap only when `max` is not that one, so a reconnect of the same instance
|
|
384
|
-
* leaves the stack as it was unless `max` changed while it was away.
|
|
385
|
-
*/
|
|
339
|
+
#arrivals = /* @__PURE__ */ new Set();
|
|
386
340
|
#appliedMax = null;
|
|
387
|
-
/** Messages already processed, in insertion order, to enforce `max` and avoid double work. */
|
|
388
341
|
#order = [];
|
|
389
|
-
/**
|
|
390
|
-
* Messages between `leaving` and their removal. {@link FlashController.#beginDismiss}
|
|
391
|
-
* releases the bookkeeping above *before* the transition wait, so for that window the
|
|
392
|
-
* element is in the DOM but in neither collection — without this set a re-scan would
|
|
393
|
-
* read it as a brand-new flash and show it a second time.
|
|
394
|
-
*/
|
|
395
342
|
#leaving = /* @__PURE__ */ new Set();
|
|
396
|
-
#
|
|
343
|
+
get #shownAttribute() {
|
|
344
|
+
return `data-${this.identifier}-${SHOWN_ATTRIBUTE}`;
|
|
345
|
+
}
|
|
397
346
|
#onEnter = (event) => this.#pause(event.currentTarget, event.type === "focusin" ? "focus" : "hover");
|
|
398
|
-
/**
|
|
399
|
-
* Releases hover or focus on a message. Focus moving between two of the message's
|
|
400
|
-
* own controls is no release, since `focusout` names a control still inside it; and
|
|
401
|
-
* the message the pointer or focus is moving into is spared when the release
|
|
402
|
-
* applies the cap again, because its own hold only arrives after this event.
|
|
403
|
-
*/
|
|
404
347
|
#onLeave = (event) => {
|
|
405
348
|
const message = event.currentTarget;
|
|
406
349
|
const next = ownerOf(this.#order, event.relatedTarget);
|
|
@@ -409,25 +352,28 @@ var FlashController = class extends Controller {
|
|
|
409
352
|
};
|
|
410
353
|
connect() {
|
|
411
354
|
this.#connected = true;
|
|
412
|
-
this
|
|
355
|
+
const current = this.messageTargets;
|
|
356
|
+
for (const item of this.#arrivals) {
|
|
357
|
+
if (!current.includes(item)) this.#arrivals.delete(item);
|
|
358
|
+
}
|
|
359
|
+
this.#discardInherited();
|
|
360
|
+
this.#reapply.observe(this.element);
|
|
413
361
|
for (const message of this.messageTargets) {
|
|
414
362
|
if (this.#owns(message)) this.#process(message, true);
|
|
415
363
|
}
|
|
416
|
-
if (!Object.is(this.#appliedMax, this
|
|
417
|
-
this.#appliedMax = this
|
|
364
|
+
if (!Object.is(this.#appliedMax, this.#safeMax)) {
|
|
365
|
+
this.#appliedMax = this.#safeMax;
|
|
418
366
|
this.#enforceMax();
|
|
419
367
|
}
|
|
420
368
|
this.#syncObservation();
|
|
421
|
-
this.#beforeCache.activate();
|
|
422
369
|
}
|
|
423
370
|
disconnect() {
|
|
424
371
|
this.#connected = false;
|
|
425
|
-
this.#beforeCache.deactivate();
|
|
426
372
|
this.#stopObserving();
|
|
427
373
|
this.#timers.clearAll();
|
|
428
374
|
this.#dismiss.clearAll();
|
|
429
375
|
this.#fixedDismiss.clearAll();
|
|
430
|
-
this.#reapply.
|
|
376
|
+
this.#reapply.disconnect();
|
|
431
377
|
this.#spared = [];
|
|
432
378
|
this.#pointer.dispose();
|
|
433
379
|
this.#awaitingPointer.clear();
|
|
@@ -435,66 +381,30 @@ var FlashController = class extends Controller {
|
|
|
435
381
|
this.#order.length = 0;
|
|
436
382
|
this.#leaving.clear();
|
|
437
383
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
* too — that value governs the timer, not what belongs in a cached page. Removal
|
|
444
|
-
* only: `dismiss` reports a dismissal, and freezing the page is not one.
|
|
445
|
-
*/
|
|
446
|
-
#rewindForCache() {
|
|
447
|
-
const removed = this.#order.length;
|
|
448
|
-
for (const message of [...this.#order]) {
|
|
384
|
+
#discardInherited() {
|
|
385
|
+
let removed = 0;
|
|
386
|
+
for (const message of this.messageTargets) {
|
|
387
|
+
if (!message.hasAttribute(this.#shownAttribute) || this.#arrivals.has(message)) continue;
|
|
388
|
+
if (!this.#owns(message)) continue;
|
|
449
389
|
message.remove();
|
|
450
|
-
|
|
390
|
+
removed += 1;
|
|
451
391
|
}
|
|
452
|
-
for (const message of this.#leaving) message.remove();
|
|
453
|
-
this.#leaving.clear();
|
|
454
392
|
if (removed > 0) this.dispatch("reconcile", { detail: { removed } });
|
|
455
393
|
}
|
|
456
|
-
/** Follows a `region` element swapped in — or arriving — at runtime (Turbo Stream). */
|
|
457
394
|
regionTargetConnected() {
|
|
458
395
|
this.#resync();
|
|
459
396
|
}
|
|
460
|
-
/** Releases the observation when the `region` element leaves the target set. */
|
|
461
397
|
regionTargetDisconnected() {
|
|
462
398
|
this.#resync();
|
|
463
399
|
}
|
|
464
|
-
/**
|
|
465
|
-
* Holds the stack to a changed `max`: lowering it below the count dismisses the
|
|
466
|
-
* oldest messages hover and focus do not hold, with reason `limit`, as an arrival
|
|
467
|
-
* past the cap does, while raising it or setting `0` dismisses nothing. Stimulus can
|
|
468
|
-
* also call this ahead of `connect()` — for an attribute that changed while the
|
|
469
|
-
* controller was away, and on every connect for an undeclared `max`, with its default;
|
|
470
|
-
* `connect()` decides then.
|
|
471
|
-
*/
|
|
472
400
|
maxValueChanged() {
|
|
473
401
|
if (!this.#connected) return;
|
|
474
|
-
this.#appliedMax = this
|
|
402
|
+
this.#appliedMax = this.#safeMax;
|
|
475
403
|
this.#enforceMax();
|
|
476
404
|
}
|
|
477
|
-
/**
|
|
478
|
-
* Whether this controller owns `message`. Ownership is the current `region`'s
|
|
479
|
-
* subtree: a message target anywhere else in the controller's scope is the
|
|
480
|
-
* consumer's, and so is one in a region that has gone away. The initial scan, a
|
|
481
|
-
* re-scan after a `region` swap, and a departure from the target set all resolve
|
|
482
|
-
* ownership through this one test; the observation gets it structurally, by watching
|
|
483
|
-
* that subtree and nothing else.
|
|
484
|
-
*/
|
|
485
405
|
#owns(message) {
|
|
486
406
|
return this.hasRegionTarget && this.regionTarget.contains(message);
|
|
487
407
|
}
|
|
488
|
-
/**
|
|
489
|
-
* Releases a message that left the target set (a Turbo Stream `remove`, the consumer
|
|
490
|
-
* detaching the node, or a morph that rewrote the target attribute in place): it
|
|
491
|
-
* stops occupying a `max` slot, and both its pending auto-dismiss and an already
|
|
492
|
-
* scheduled removal are cancelled. A move *within* the region keeps all of them —
|
|
493
|
-
* which is why the element must still be a message to be treated as one: ownership
|
|
494
|
-
* alone reads an in-place attribute rewrite as a move, and a node outside the target
|
|
495
|
-
* set belongs to the consumer, so nothing here may dismiss it. A move may still have
|
|
496
|
-
* ended a hold, so those are read again.
|
|
497
|
-
*/
|
|
498
408
|
messageTargetDisconnected(message) {
|
|
499
409
|
if (!this.#connected) return;
|
|
500
410
|
const moved = this.#owns(message) && message.matches(this.#messageSelector);
|
|
@@ -503,15 +413,9 @@ var FlashController = class extends Controller {
|
|
|
503
413
|
return;
|
|
504
414
|
}
|
|
505
415
|
this.#forget(message);
|
|
416
|
+
this.#arrivals.delete(message);
|
|
506
417
|
this.#leaving.delete(message);
|
|
507
418
|
}
|
|
508
|
-
/**
|
|
509
|
-
* Re-points the observation after a `region` swap and picks up the messages the
|
|
510
|
-
* new element brought with it (dynamic inserts, so their own `role` announces
|
|
511
|
-
* them). The `#connected` guard is load-bearing: Stimulus runs target callbacks
|
|
512
|
-
* for the initial markup *before* `connect()` and again during teardown *after*
|
|
513
|
-
* `disconnect()`, and re-observing there would outlive the controller.
|
|
514
|
-
*/
|
|
515
419
|
#resync() {
|
|
516
420
|
if (!this.#connected) return;
|
|
517
421
|
this.#syncObservation();
|
|
@@ -519,11 +423,6 @@ var FlashController = class extends Controller {
|
|
|
519
423
|
if (this.#owns(message)) this.#process(message, false);
|
|
520
424
|
}
|
|
521
425
|
}
|
|
522
|
-
/**
|
|
523
|
-
* Points the mutation observation at the current `region` target, re-resolved on
|
|
524
|
-
* every sync rather than captured at connect, so an element swapped in at runtime
|
|
525
|
-
* is observed instead of the detached original.
|
|
526
|
-
*/
|
|
527
426
|
#syncObservation() {
|
|
528
427
|
this.#stopObserving();
|
|
529
428
|
if (!this.hasRegionTarget || typeof MutationObserver === "undefined") return;
|
|
@@ -534,11 +433,6 @@ var FlashController = class extends Controller {
|
|
|
534
433
|
this.#observer?.disconnect();
|
|
535
434
|
this.#observer = null;
|
|
536
435
|
}
|
|
537
|
-
/**
|
|
538
|
-
* Hover and focus listeners, bound and unbound as a pair so the two sides stay in
|
|
539
|
-
* sync. Every message gets them, whatever `pauseOnHover` says, because the cap reads
|
|
540
|
-
* the holds they record; unbinding is idempotent (a no-op when nothing was bound).
|
|
541
|
-
*/
|
|
542
436
|
#bindPause(message) {
|
|
543
437
|
message.addEventListener("mouseenter", this.#onEnter);
|
|
544
438
|
message.addEventListener("mouseleave", this.#onLeave);
|
|
@@ -551,13 +445,14 @@ var FlashController = class extends Controller {
|
|
|
551
445
|
message.removeEventListener("focusin", this.#onEnter);
|
|
552
446
|
message.removeEventListener("focusout", this.#onLeave);
|
|
553
447
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
448
|
+
dismiss(input) {
|
|
449
|
+
const source = actionSource(input);
|
|
450
|
+
if (source.origin && source.origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
451
|
+
return;
|
|
452
|
+
const message = (source.origin ?? source.host)?.closest(this.#messageSelector);
|
|
453
|
+
if (message && this.messageTargets.includes(message))
|
|
454
|
+
this.#beginDismiss(message, source.reason);
|
|
559
455
|
}
|
|
560
|
-
/** Processes messages added after connect (Turbo Stream); their own role announces them. */
|
|
561
456
|
#onMutations(mutations) {
|
|
562
457
|
for (const mutation of mutations) {
|
|
563
458
|
for (const node of mutation.addedNodes) {
|
|
@@ -569,14 +464,6 @@ var FlashController = class extends Controller {
|
|
|
569
464
|
}
|
|
570
465
|
}
|
|
571
466
|
}
|
|
572
|
-
/**
|
|
573
|
-
* Applies role/state, wires pause listeners, takes the holds the message already has,
|
|
574
|
-
* and schedules auto-dismiss. A message `connect()` takes on (`atConnect`) is bridged
|
|
575
|
-
* to the Announcer and meets no cap here, `connect()` holding the stack to it; a later
|
|
576
|
-
* insert announces through its own role and meets the cap as it arrives. Idempotent
|
|
577
|
-
* per message, and a node that has left the region by the time its insertion is
|
|
578
|
-
* reported is not taken on.
|
|
579
|
-
*/
|
|
580
467
|
#process(message, atConnect) {
|
|
581
468
|
if (this.#order.includes(message) || this.#leaving.has(message) || !this.#owns(message)) {
|
|
582
469
|
return;
|
|
@@ -587,110 +474,74 @@ var FlashController = class extends Controller {
|
|
|
587
474
|
message.setAttribute("role", assertive ? "alert" : "status");
|
|
588
475
|
}
|
|
589
476
|
message.setAttribute("data-flash-state", "visible");
|
|
477
|
+
message.setAttribute(this.#shownAttribute, "");
|
|
478
|
+
const known = this.#arrivals.has(message);
|
|
590
479
|
this.#order.push(message);
|
|
480
|
+
this.#arrivals.add(message);
|
|
591
481
|
this.#bindPause(message);
|
|
592
482
|
this.#readHolds(message);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
483
|
+
if (!known) {
|
|
484
|
+
const text = message.textContent?.trim() ?? "";
|
|
485
|
+
this.dispatch("show", { target: message, detail: { type, message: text } });
|
|
486
|
+
if (atConnect) announce(text, { assertive });
|
|
487
|
+
}
|
|
596
488
|
this.#startTimer(message);
|
|
597
489
|
if (!atConnect) this.#enforceMax([message]);
|
|
598
490
|
}
|
|
599
|
-
/**
|
|
600
|
-
* Dismisses the oldest messages with reason `limit` while more than `max` are shown
|
|
601
|
-
* (0 or less, or not a finite number, means no cap), passing over every message hover
|
|
602
|
-
* or focus holds and those in `spared` — the message just taken on, or the ones the
|
|
603
|
-
* pointer or focus moved into. Only messages still in the region count or go: one a
|
|
604
|
-
* script has just taken out stays on the stack until its removal is reported. With
|
|
605
|
-
* nothing else left to go the stack stays over the cap. The loop walks a copy and
|
|
606
|
-
* re-reads the count on every step, because a `dismiss` listener may already have
|
|
607
|
-
* changed both.
|
|
608
|
-
*
|
|
609
|
-
* @stimeoRenderRoot
|
|
610
|
-
*/
|
|
611
491
|
#enforceMax(spared = []) {
|
|
612
|
-
if (
|
|
613
|
-
for (const message of [...this.#
|
|
614
|
-
if (this.#shownCount() <= this
|
|
492
|
+
if (this.#safeMax <= 0) return;
|
|
493
|
+
for (const message of [...this.#arrivals]) {
|
|
494
|
+
if (this.#shownCount() <= this.#safeMax) return;
|
|
615
495
|
if (this.#owns(message) && !spared.includes(message) && !this.#dismiss.isHeld(message)) {
|
|
616
496
|
this.#beginDismiss(message, "limit");
|
|
617
497
|
}
|
|
618
498
|
}
|
|
619
499
|
}
|
|
620
|
-
/** How many messages the stack shows: the ones taken on that are still in the region. */
|
|
621
500
|
#shownCount() {
|
|
622
501
|
return this.#order.filter((message) => this.#owns(message)).length;
|
|
623
502
|
}
|
|
624
|
-
/**
|
|
625
|
-
* Arms a message's auto-dismiss; a non-positive `duration` means it never expires.
|
|
626
|
-
* The timer waits out hover and focus when `pauseOnHover` is on, and runs on
|
|
627
|
-
* regardless when it is off.
|
|
628
|
-
*
|
|
629
|
-
* @stimeoRuntimeOnly `duration` is the delay of the one dismissal timer this call arms,
|
|
630
|
-
* and `pauseOnHover` whether hover and focus hold that timer.
|
|
631
|
-
*/
|
|
632
503
|
#startTimer(message) {
|
|
633
|
-
if (this
|
|
504
|
+
if (this.#safeDuration <= 0) return;
|
|
634
505
|
const dismiss = () => this.#beginDismiss(message, "timeout");
|
|
635
|
-
if (this.pauseOnHoverValue) this.#dismiss.set(message, dismiss, this
|
|
636
|
-
else this.#fixedDismiss.set(message, dismiss, this
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Records hover or focus on a message. Hover and focus are independent reasons:
|
|
640
|
-
* the registry banks the time left on the first of them and waits for the last
|
|
641
|
-
* (WCAG 2.2 2.2.1), and the cap passes over the message while either is held.
|
|
642
|
-
*/
|
|
506
|
+
if (this.pauseOnHoverValue) this.#dismiss.set(message, dismiss, this.#safeDuration);
|
|
507
|
+
else this.#fixedDismiss.set(message, dismiss, this.#safeDuration);
|
|
508
|
+
}
|
|
643
509
|
#pause(message, reason) {
|
|
644
510
|
this.#dismiss.pause(message, reason);
|
|
645
511
|
}
|
|
646
|
-
/**
|
|
647
|
-
* Releases one reason, resuming the banked time once no reason is left. Releasing
|
|
648
|
-
* the last one applies the cap again, sparing `spare`.
|
|
649
|
-
*/
|
|
650
512
|
#resume(message, reason, spare) {
|
|
651
513
|
const held = this.#dismiss.isHeld(message);
|
|
652
514
|
this.#dismiss.resume(message, reason);
|
|
653
515
|
if (held && !this.#dismiss.isHeld(message)) this.#capLater(spare);
|
|
654
516
|
}
|
|
655
|
-
/**
|
|
656
|
-
* Applies the cap again once the current event is over, sparing `spare`. A hold can
|
|
657
|
-
* end in a `focusout` the engine fires while it is itself taking the node out for a
|
|
658
|
-
* script's move or removal, and dismissing the message inside that event would pull
|
|
659
|
-
* the node from under the caller's own call.
|
|
660
|
-
*/
|
|
661
517
|
#capLater(spare) {
|
|
662
518
|
if (spare) this.#spared.push(spare);
|
|
663
519
|
this.#reapply.schedule();
|
|
664
520
|
}
|
|
665
|
-
/** Applies the cap again, sparing every message gathered since the last pass. */
|
|
666
521
|
#reapplyCap() {
|
|
522
|
+
for (const message of [...this.#order, ...this.#leaving]) {
|
|
523
|
+
if (!this.#owns(message)) continue;
|
|
524
|
+
message.setAttribute("data-flash-state", this.#leaving.has(message) ? "leaving" : "visible");
|
|
525
|
+
message.setAttribute(this.#shownAttribute, "");
|
|
526
|
+
if (!message.hasAttribute("role")) {
|
|
527
|
+
const assertive = ASSERTIVE_TYPES.has(message.getAttribute("data-flash-type") ?? "");
|
|
528
|
+
message.setAttribute("role", assertive ? "alert" : "status");
|
|
529
|
+
}
|
|
530
|
+
}
|
|
667
531
|
const spared = this.#spared;
|
|
668
532
|
this.#spared = [];
|
|
669
533
|
this.#enforceMax(spared);
|
|
670
534
|
}
|
|
671
|
-
/**
|
|
672
|
-
* Reads the holds of a message that moved within the region. Focus is read at once:
|
|
673
|
-
* an ordinary move ends it with a `focusout` and `moveBefore` keeps it. Where the
|
|
674
|
-
* pointer is, nothing says until it moves again: no `mouseleave` reaches a moved node,
|
|
675
|
-
* and `:hover` can answer from before the move. A message still held waits for that
|
|
676
|
-
* movement.
|
|
677
|
-
*/
|
|
678
535
|
#afterMove(message) {
|
|
679
536
|
if (!message.contains(document.activeElement)) this.#resume(message, "focus", null);
|
|
680
537
|
if (this.#dismiss.isHeld(message)) this.#awaitPointer(message);
|
|
681
538
|
}
|
|
682
|
-
/**
|
|
683
|
-
* Takes the holds a message already has as it is taken on: focus inside it, and the
|
|
684
|
-
* pointer over it by `:hover`. That answer can date from before the message came here,
|
|
685
|
-
* so a hover hold taken on it waits for the next pointer movement to be confirmed.
|
|
686
|
-
*/
|
|
687
539
|
#readHolds(message) {
|
|
688
540
|
if (message.contains(document.activeElement)) this.#pause(message, "focus");
|
|
689
541
|
if (!message.matches(":hover")) return;
|
|
690
542
|
this.#pause(message, "hover");
|
|
691
543
|
this.#awaitPointer(message);
|
|
692
544
|
}
|
|
693
|
-
/** Leaves `message`'s hover hold to be confirmed or let go on the next pointer movement. */
|
|
694
545
|
#awaitPointer(message) {
|
|
695
546
|
this.#awaitingPointer.add(message);
|
|
696
547
|
this.#pointer.add(document, "pointermove", this.#onPointerMove, {
|
|
@@ -698,10 +549,6 @@ var FlashController = class extends Controller {
|
|
|
698
549
|
passive: true
|
|
699
550
|
});
|
|
700
551
|
}
|
|
701
|
-
/**
|
|
702
|
-
* Reads `:hover` once the pointer has moved, when the answer is current, and lets go of
|
|
703
|
-
* the hover hold of every waiting message the pointer is not over. Listens once.
|
|
704
|
-
*/
|
|
705
552
|
#onPointerMove = () => {
|
|
706
553
|
this.#pointer.dispose();
|
|
707
554
|
const waiting = [...this.#awaitingPointer];
|
|
@@ -710,11 +557,6 @@ var FlashController = class extends Controller {
|
|
|
710
557
|
if (!message.matches(":hover")) this.#resume(message, "hover", null);
|
|
711
558
|
}
|
|
712
559
|
};
|
|
713
|
-
/**
|
|
714
|
-
* Releases every per-message resource: timers, hold, stacking slot, pause listeners,
|
|
715
|
-
* and the wait on the pointer. A message something still held counts as released: the
|
|
716
|
-
* cap is applied again.
|
|
717
|
-
*/
|
|
718
560
|
#forget(message) {
|
|
719
561
|
if (this.#dismiss.isHeld(message)) this.#capLater(null);
|
|
720
562
|
this.#dismiss.clear(message);
|
|
@@ -725,7 +567,6 @@ var FlashController = class extends Controller {
|
|
|
725
567
|
this.#awaitingPointer.delete(message);
|
|
726
568
|
if (this.#awaitingPointer.size === 0) this.#pointer.dispose();
|
|
727
569
|
}
|
|
728
|
-
/** Marks a message leaving, then removes it after its CSS transition and emits dismiss. */
|
|
729
570
|
#beginDismiss(message, reason) {
|
|
730
571
|
if (!this.#order.includes(message)) return;
|
|
731
572
|
this.#forget(message);
|
|
@@ -734,6 +575,7 @@ var FlashController = class extends Controller {
|
|
|
734
575
|
const finalize = () => {
|
|
735
576
|
if (!this.#leaving.delete(message)) return;
|
|
736
577
|
message.remove();
|
|
578
|
+
this.#arrivals.delete(message);
|
|
737
579
|
this.dispatch("dismiss", { detail: { element: message, reason } });
|
|
738
580
|
};
|
|
739
581
|
const transition = this.#transitionMs(message);
|
|
@@ -743,13 +585,29 @@ var FlashController = class extends Controller {
|
|
|
743
585
|
finalize();
|
|
744
586
|
}
|
|
745
587
|
}
|
|
746
|
-
/** Maximum transition total (duration + delay) of `el` in ms (0 when none / unsupported). */
|
|
747
588
|
#transitionMs(el) {
|
|
748
589
|
if (typeof window.getComputedStyle !== "function") return 0;
|
|
749
590
|
return maxTransitionTotalMs(window.getComputedStyle(el));
|
|
750
591
|
}
|
|
592
|
+
get #safeDuration() {
|
|
593
|
+
return this.#numbers.read(
|
|
594
|
+
this,
|
|
595
|
+
"duration",
|
|
596
|
+
this.durationValue,
|
|
597
|
+
_FlashController.values.duration.default,
|
|
598
|
+
_FlashController.valueConstraints.duration
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
get #safeMax() {
|
|
602
|
+
return this.#numbers.read(
|
|
603
|
+
this,
|
|
604
|
+
"max",
|
|
605
|
+
this.maxValue,
|
|
606
|
+
_FlashController.values.max.default,
|
|
607
|
+
_FlashController.valueConstraints.max
|
|
608
|
+
);
|
|
609
|
+
}
|
|
751
610
|
};
|
|
752
611
|
|
|
753
612
|
export { FlashController };
|
|
754
|
-
//# sourceMappingURL=flash_controller.js.map
|
|
755
613
|
//# sourceMappingURL=flash_controller.js.map
|