stimeo-ui 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +209 -0
- data/README.md +128 -1
- data/dist/cable/index.js +392 -417
- data/dist/controllers/accordion_controller.js +228 -28
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -191
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +295 -110
- data/dist/controllers/avatar_controller.js +172 -106
- data/dist/controllers/breadcrumb_controller.js +0 -129
- data/dist/controllers/bulk_select_controller.js +264 -53
- data/dist/controllers/calendar_controller.js +346 -180
- data/dist/controllers/carousel_controller.js +587 -300
- data/dist/controllers/character_counter_controller.js +271 -143
- data/dist/controllers/checkbox_controller.js +77 -75
- data/dist/controllers/clipboard_controller.js +187 -108
- data/dist/controllers/collapsible_controller.js +374 -108
- data/dist/controllers/color_picker_controller.js +183 -127
- data/dist/controllers/combobox_controller.js +426 -109
- data/dist/controllers/command_palette_controller.js +994 -350
- data/dist/controllers/conditional_fields_controller.js +212 -125
- data/dist/controllers/confirm_controller.js +939 -245
- data/dist/controllers/context_menu_controller.js +275 -127
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +235 -97
- data/dist/controllers/currency_input_controller.js +241 -118
- data/dist/controllers/data_grid_controller.js +229 -135
- data/dist/controllers/date_range_picker_controller.js +359 -165
- data/dist/controllers/dialog_controller.js +887 -225
- data/dist/controllers/direct_upload_controller.js +80 -142
- data/dist/controllers/dirty_form_controller.js +26 -47
- data/dist/controllers/dismissible_controller.js +175 -20
- data/dist/controllers/drawer_controller.js +963 -336
- data/dist/controllers/dropdown_controller.js +342 -85
- data/dist/controllers/editable_controller.js +182 -58
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +426 -229
- data/dist/controllers/filter_controller.js +78 -36
- data/dist/controllers/flash_controller.js +328 -211
- data/dist/controllers/focus_controller.js +654 -239
- data/dist/controllers/form_field_controller.js +153 -136
- data/dist/controllers/form_validation_controller.js +29 -96
- data/dist/controllers/frame_loading_controller.js +263 -198
- data/dist/controllers/highlight_controller.js +119 -70
- data/dist/controllers/hover_card_controller.js +335 -118
- data/dist/controllers/idle_controller.js +316 -65
- data/dist/controllers/input_mask_controller.js +116 -61
- data/dist/controllers/intersection_controller.js +147 -103
- data/dist/controllers/lazy_frame_controller.js +9 -56
- data/dist/controllers/listbox_controller.js +413 -153
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -95
- data/dist/controllers/menu_controller.js +254 -167
- data/dist/controllers/menubar_controller.js +206 -331
- data/dist/controllers/meter_controller.js +159 -71
- data/dist/controllers/multi_select_controller.js +797 -388
- data/dist/controllers/navigation_menu_controller.js +156 -231
- data/dist/controllers/nested_form_controller.js +373 -174
- data/dist/controllers/network_status_controller.js +241 -45
- data/dist/controllers/number_input_controller.js +387 -227
- data/dist/controllers/optimistic_controller.js +28 -75
- data/dist/controllers/otp_controller.js +344 -196
- data/dist/controllers/overflow_indicator_controller.js +167 -93
- data/dist/controllers/overflow_menu_controller.js +408 -277
- data/dist/controllers/pagination_controller.js +342 -120
- data/dist/controllers/password_reveal_controller.js +424 -99
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +252 -96
- data/dist/controllers/pointer_drag_controller.js +263 -164
- data/dist/controllers/popover_controller.js +267 -94
- data/dist/controllers/portal_controller.js +209 -72
- data/dist/controllers/preview_guard_controller.js +243 -112
- data/dist/controllers/progress_controller.js +132 -59
- data/dist/controllers/radio_group_controller.js +138 -133
- data/dist/controllers/range_slider_controller.js +245 -112
- data/dist/controllers/rating_controller.js +429 -194
- data/dist/controllers/read_more_controller.js +185 -47
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +294 -118
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -221
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -146
- data/dist/controllers/scrollspy_controller.js +181 -236
- data/dist/controllers/separator_controller.js +288 -132
- data/dist/controllers/sidebar_controller.js +1071 -375
- data/dist/controllers/skeleton_controller.js +248 -92
- data/dist/controllers/slider_controller.js +211 -96
- data/dist/controllers/smart_sticky_header_controller.js +163 -54
- data/dist/controllers/sortable_controller.js +13 -110
- data/dist/controllers/spinner_controller.js +277 -161
- data/dist/controllers/step_indicator_controller.js +107 -53
- data/dist/controllers/stepper_controller.js +171 -48
- data/dist/controllers/stick_to_bottom_controller.js +310 -116
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -192
- data/dist/controllers/switch_controller.js +107 -43
- data/dist/controllers/tabs_controller.js +50 -25
- data/dist/controllers/tags_input_controller.js +290 -149
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +148 -80
- data/dist/controllers/time_picker_controller.js +162 -81
- data/dist/controllers/toast_controller.js +365 -193
- data/dist/controllers/toggle_group_controller.js +194 -99
- data/dist/controllers/toolbar_controller.js +92 -90
- data/dist/controllers/tooltip_controller.js +185 -125
- data/dist/controllers/transition_controller.js +123 -115
- data/dist/controllers/tree_view_controller.js +248 -290
- data/dist/index.js +8649 -8572
- data/dist/positioning/index.js +148 -45
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,8 +1,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,72 +32,34 @@ function announce(message, options = {}) {
|
|
|
13
32
|
);
|
|
14
33
|
}
|
|
15
34
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
};
|
|
35
|
+
function ownerIndex(candidates, node) {
|
|
36
|
+
if (!(node instanceof Node)) return -1;
|
|
37
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
38
|
+
}
|
|
39
|
+
function ownerOf(candidates, node) {
|
|
40
|
+
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
44
44
|
|
|
45
|
-
// src/utils/safe_timeout.ts
|
|
46
45
|
var TimerRegistry = class {
|
|
47
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
48
46
|
ids = /* @__PURE__ */ new Set();
|
|
49
|
-
/**
|
|
50
|
-
* Cancels a single tracked timer.
|
|
51
|
-
*
|
|
52
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
53
|
-
* registry), so callers can clear defensively without guarding.
|
|
54
|
-
*/
|
|
55
47
|
clear(id) {
|
|
56
48
|
if (this.ids.delete(id)) {
|
|
57
49
|
this.cancel(id);
|
|
58
50
|
}
|
|
59
51
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
62
|
-
* to guarantee no timer outlives the element.
|
|
63
|
-
*/
|
|
64
52
|
clearAll() {
|
|
65
53
|
for (const id of this.ids) {
|
|
66
54
|
this.cancel(id);
|
|
67
55
|
}
|
|
68
56
|
this.ids.clear();
|
|
69
57
|
}
|
|
70
|
-
/** Number of timers currently tracked (pending). */
|
|
71
58
|
get size() {
|
|
72
59
|
return this.ids.size;
|
|
73
60
|
}
|
|
74
61
|
};
|
|
75
62
|
var SafeTimeout = class extends TimerRegistry {
|
|
76
|
-
/**
|
|
77
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
78
|
-
*
|
|
79
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
80
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
81
|
-
*/
|
|
82
63
|
set(callback, delay) {
|
|
83
64
|
const id = this.schedule(() => {
|
|
84
65
|
this.ids.delete(id);
|
|
@@ -95,16 +76,9 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
95
76
|
}
|
|
96
77
|
};
|
|
97
78
|
|
|
98
|
-
// src/utils/keyed_timers.ts
|
|
99
79
|
var KeyedTimers = class {
|
|
100
80
|
#timers = new SafeTimeout();
|
|
101
|
-
/** The pending timer of each key; an entry lives exactly as long as its timer. */
|
|
102
81
|
#pending = /* @__PURE__ */ new Map();
|
|
103
|
-
/**
|
|
104
|
-
* Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had
|
|
105
|
-
* pending. The entry is dropped before the callback runs, so the callback sees
|
|
106
|
-
* the key unarmed and may arm it again for the next round.
|
|
107
|
-
*/
|
|
108
82
|
set(key, callback, delay) {
|
|
109
83
|
this.clear(key);
|
|
110
84
|
const id = this.#timers.set(() => {
|
|
@@ -113,104 +87,193 @@ var KeyedTimers = class {
|
|
|
113
87
|
}, delay);
|
|
114
88
|
this.#pending.set(key, id);
|
|
115
89
|
}
|
|
116
|
-
/**
|
|
117
|
-
* Cancels `key`'s pending timer, if it has one.
|
|
118
|
-
*
|
|
119
|
-
* A timer id is a positive integer, so `-1` stands for "nothing pending" and
|
|
120
|
-
* the registry ignores an id it does not own — the unarmed case needs no
|
|
121
|
-
* branch of its own, and no other key's timer can be reached from here.
|
|
122
|
-
*/
|
|
123
90
|
clear(key) {
|
|
124
91
|
this.#timers.clear(this.#pending.get(key) ?? -1);
|
|
125
92
|
this.#pending.delete(key);
|
|
126
93
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Cancels every pending timer and forgets every key. Call this from a
|
|
129
|
-
* controller's `disconnect()` so no timer, and no entry, outlives the element.
|
|
130
|
-
*/
|
|
131
94
|
clearAll() {
|
|
132
95
|
this.#timers.clearAll();
|
|
133
96
|
this.#pending.clear();
|
|
134
97
|
}
|
|
135
|
-
/** Whether `key` has a timer pending. */
|
|
136
98
|
has(key) {
|
|
137
99
|
return this.#pending.has(key);
|
|
138
100
|
}
|
|
139
101
|
};
|
|
140
102
|
|
|
141
|
-
|
|
103
|
+
var ListenerSet = class {
|
|
104
|
+
#abort = new AbortController();
|
|
105
|
+
add(target, type, handler, options) {
|
|
106
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
107
|
+
}
|
|
108
|
+
dispose() {
|
|
109
|
+
this.#abort.abort();
|
|
110
|
+
this.#abort = new AbortController();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
var MicrotaskCoalescer = class {
|
|
115
|
+
#run;
|
|
116
|
+
#queued = false;
|
|
117
|
+
#active = false;
|
|
118
|
+
#generation = 0;
|
|
119
|
+
constructor(run) {
|
|
120
|
+
this.#run = run;
|
|
121
|
+
}
|
|
122
|
+
activate() {
|
|
123
|
+
this.#active = true;
|
|
124
|
+
}
|
|
125
|
+
cancel() {
|
|
126
|
+
this.#active = false;
|
|
127
|
+
this.#queued = false;
|
|
128
|
+
this.#generation += 1;
|
|
129
|
+
}
|
|
130
|
+
schedule() {
|
|
131
|
+
if (!this.#active || this.#queued) return;
|
|
132
|
+
this.#queued = true;
|
|
133
|
+
const generation = this.#generation;
|
|
134
|
+
queueMicrotask(() => {
|
|
135
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
136
|
+
this.#queued = false;
|
|
137
|
+
this.#run();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
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
|
+
|
|
142
212
|
var PausableTimers = class {
|
|
143
213
|
#timers = new KeyedTimers();
|
|
144
|
-
/** Every key armed or held; an entry goes as its timer fires, or with the key. */
|
|
145
214
|
#entries = /* @__PURE__ */ new Map();
|
|
146
|
-
/**
|
|
147
|
-
* Arms `callback` after `delay` ms for `key`, replacing whatever `key` had.
|
|
148
|
-
* A held key stays held and banks `delay` for its resume.
|
|
149
|
-
*/
|
|
150
215
|
set(key, callback, delay) {
|
|
151
216
|
const reasons = this.#entries.get(key)?.reasons ?? /* @__PURE__ */ new Set();
|
|
152
217
|
const entry = { callback, startedAt: 0, remaining: delay, reasons };
|
|
153
218
|
this.#entries.set(key, entry);
|
|
154
|
-
if (reasons.size === 0) this.#arm(key, entry);
|
|
219
|
+
if (reasons.size === 0) this.#arm(key, entry, callback);
|
|
155
220
|
}
|
|
156
|
-
/**
|
|
157
|
-
* Holds `key` for `reason` and banks the time left, with a floor of one
|
|
158
|
-
* millisecond. Reports whether this call is the one that stopped a running
|
|
159
|
-
* timer, so the caller can write its held-state hook exactly once.
|
|
160
|
-
*/
|
|
161
221
|
pause(key, reason) {
|
|
162
|
-
|
|
163
|
-
if (entry === void 0)
|
|
222
|
+
let entry = this.#entries.get(key);
|
|
223
|
+
if (entry === void 0) {
|
|
224
|
+
entry = { callback: null, startedAt: 0, remaining: 0, reasons: /* @__PURE__ */ new Set() };
|
|
225
|
+
this.#entries.set(key, entry);
|
|
226
|
+
}
|
|
164
227
|
entry.reasons.add(reason);
|
|
165
228
|
if (!this.#timers.has(key)) return false;
|
|
166
229
|
this.#timers.clear(key);
|
|
167
230
|
entry.remaining = Math.max(1, entry.remaining - (Date.now() - entry.startedAt));
|
|
168
231
|
return true;
|
|
169
232
|
}
|
|
170
|
-
/**
|
|
171
|
-
* Releases `reason` on `key`, arming the banked time again once no reason is
|
|
172
|
-
* left. Reports whether this call is the one that started the timer again.
|
|
173
|
-
*/
|
|
174
233
|
resume(key, reason) {
|
|
175
234
|
const entry = this.#entries.get(key);
|
|
176
235
|
if (entry === void 0) return false;
|
|
177
236
|
entry.reasons.delete(reason);
|
|
178
237
|
if (entry.reasons.size > 0 || this.#timers.has(key)) return false;
|
|
179
|
-
|
|
238
|
+
const { callback } = entry;
|
|
239
|
+
if (callback === null) {
|
|
240
|
+
this.#entries.delete(key);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
this.#arm(key, entry, callback);
|
|
180
244
|
return true;
|
|
181
245
|
}
|
|
182
|
-
|
|
246
|
+
isHeld(key) {
|
|
247
|
+
return (this.#entries.get(key)?.reasons.size ?? 0) > 0;
|
|
248
|
+
}
|
|
183
249
|
tracks(key) {
|
|
184
250
|
return this.#entries.has(key);
|
|
185
251
|
}
|
|
186
|
-
/** Cancels `key`'s timer and drops its hold. */
|
|
187
252
|
clear(key) {
|
|
188
253
|
this.#timers.clear(key);
|
|
189
254
|
this.#entries.delete(key);
|
|
190
255
|
}
|
|
191
|
-
/**
|
|
192
|
-
* Cancels every timer and forgets every key. Call this from a controller's
|
|
193
|
-
* `disconnect()` so neither a timer nor a hold outlives the element.
|
|
194
|
-
*/
|
|
195
256
|
clearAll() {
|
|
196
257
|
this.#timers.clearAll();
|
|
197
258
|
this.#entries.clear();
|
|
198
259
|
}
|
|
199
|
-
|
|
200
|
-
#arm(key, entry) {
|
|
260
|
+
#arm(key, entry, callback) {
|
|
201
261
|
entry.startedAt = Date.now();
|
|
202
262
|
this.#timers.set(
|
|
203
263
|
key,
|
|
204
264
|
() => {
|
|
205
265
|
this.#entries.delete(key);
|
|
206
|
-
|
|
266
|
+
callback();
|
|
207
267
|
},
|
|
208
268
|
entry.remaining
|
|
209
269
|
);
|
|
210
270
|
}
|
|
211
271
|
};
|
|
212
272
|
|
|
213
|
-
|
|
273
|
+
function targetSelector(identifier, name) {
|
|
274
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
275
|
+
}
|
|
276
|
+
|
|
214
277
|
function timeMs(value) {
|
|
215
278
|
const trimmed = value.trim();
|
|
216
279
|
const amount = Number.parseFloat(trimmed);
|
|
@@ -244,115 +307,115 @@ function maxTransitionTotalMs(style) {
|
|
|
244
307
|
return maxTotalMs(transitionTimings(style));
|
|
245
308
|
}
|
|
246
309
|
|
|
247
|
-
// src/controllers/flash_controller.ts
|
|
248
310
|
var ASSERTIVE_TYPES = /* @__PURE__ */ new Set(["alert", "error"]);
|
|
249
|
-
var
|
|
250
|
-
var
|
|
311
|
+
var MESSAGE_PART = "message";
|
|
312
|
+
var SHOWN_ATTRIBUTE = "shown";
|
|
313
|
+
var FlashController = class _FlashController extends Controller {
|
|
314
|
+
#numbers = new NumberValueReader();
|
|
315
|
+
get #messageSelector() {
|
|
316
|
+
return targetSelector(this.identifier, MESSAGE_PART);
|
|
317
|
+
}
|
|
251
318
|
static targets = ["region", "message"];
|
|
252
319
|
static values = {
|
|
253
320
|
duration: { type: Number, default: 5e3 },
|
|
254
321
|
pauseOnHover: { type: Boolean, default: true },
|
|
255
322
|
max: { type: Number, default: 0 }
|
|
256
323
|
};
|
|
324
|
+
static valueConstraints = {
|
|
325
|
+
duration: NUMBER_BOUNDS.timer,
|
|
326
|
+
max: NUMBER_BOUNDS.finite
|
|
327
|
+
};
|
|
257
328
|
static actions = ["dismiss"];
|
|
258
329
|
static events = ["show", "dismiss", "reconcile"];
|
|
259
|
-
/** Removal timers for the leaving transition; the auto-dismiss ones live below. */
|
|
260
330
|
#timers = new SafeTimeout();
|
|
261
|
-
/** Per-message auto-dismiss, held open while the message is hovered or focused. */
|
|
262
331
|
#dismiss = new PausableTimers();
|
|
332
|
+
#fixedDismiss = new KeyedTimers();
|
|
333
|
+
#reapply = new MorphRenderWatcher(() => this.#reapplyCap());
|
|
334
|
+
#spared = [];
|
|
335
|
+
#pointer = new ListenerSet();
|
|
336
|
+
#awaitingPointer = /* @__PURE__ */ new Set();
|
|
263
337
|
#observer = null;
|
|
264
|
-
/** Whether the controller is between `connect()` and `disconnect()`. */
|
|
265
338
|
#connected = false;
|
|
266
|
-
|
|
339
|
+
#arrivals = /* @__PURE__ */ new Set();
|
|
340
|
+
#appliedMax = null;
|
|
267
341
|
#order = [];
|
|
268
|
-
/**
|
|
269
|
-
* Messages between `leaving` and their removal. {@link FlashController.#beginDismiss}
|
|
270
|
-
* releases the bookkeeping above *before* the transition wait, so for that window the
|
|
271
|
-
* element is in the DOM but in neither collection — without this set a re-scan would
|
|
272
|
-
* read it as a brand-new flash and show it a second time.
|
|
273
|
-
*/
|
|
274
342
|
#leaving = /* @__PURE__ */ new Set();
|
|
275
|
-
#
|
|
343
|
+
get #shownAttribute() {
|
|
344
|
+
return `data-${this.identifier}-${SHOWN_ATTRIBUTE}`;
|
|
345
|
+
}
|
|
276
346
|
#onEnter = (event) => this.#pause(event.currentTarget, event.type === "focusin" ? "focus" : "hover");
|
|
277
|
-
#onLeave = (event) =>
|
|
347
|
+
#onLeave = (event) => {
|
|
348
|
+
const message = event.currentTarget;
|
|
349
|
+
const next = ownerOf(this.#order, event.relatedTarget);
|
|
350
|
+
if (next === message) return;
|
|
351
|
+
this.#resume(message, event.type === "focusout" ? "focus" : "hover", next);
|
|
352
|
+
};
|
|
278
353
|
connect() {
|
|
279
354
|
this.#connected = true;
|
|
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);
|
|
280
361
|
for (const message of this.messageTargets) {
|
|
281
362
|
if (this.#owns(message)) this.#process(message, true);
|
|
282
363
|
}
|
|
364
|
+
if (!Object.is(this.#appliedMax, this.#safeMax)) {
|
|
365
|
+
this.#appliedMax = this.#safeMax;
|
|
366
|
+
this.#enforceMax();
|
|
367
|
+
}
|
|
283
368
|
this.#syncObservation();
|
|
284
|
-
this.#beforeCache.activate();
|
|
285
369
|
}
|
|
286
370
|
disconnect() {
|
|
287
371
|
this.#connected = false;
|
|
288
|
-
this.#beforeCache.deactivate();
|
|
289
372
|
this.#stopObserving();
|
|
290
373
|
this.#timers.clearAll();
|
|
291
374
|
this.#dismiss.clearAll();
|
|
375
|
+
this.#fixedDismiss.clearAll();
|
|
376
|
+
this.#reapply.disconnect();
|
|
377
|
+
this.#spared = [];
|
|
378
|
+
this.#pointer.dispose();
|
|
379
|
+
this.#awaitingPointer.clear();
|
|
292
380
|
for (const message of this.#order) this.#unbindPause(message);
|
|
293
381
|
this.#order.length = 0;
|
|
294
382
|
this.#leaving.clear();
|
|
295
383
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
* too — that value governs the timer, not what belongs in a cached page. Removal
|
|
302
|
-
* only: `dismiss` reports a dismissal, and freezing the page is not one.
|
|
303
|
-
*/
|
|
304
|
-
#rewindForCache() {
|
|
305
|
-
const removed = this.#order.length;
|
|
306
|
-
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;
|
|
307
389
|
message.remove();
|
|
308
|
-
|
|
390
|
+
removed += 1;
|
|
309
391
|
}
|
|
310
|
-
for (const message of this.#leaving) message.remove();
|
|
311
|
-
this.#leaving.clear();
|
|
312
392
|
if (removed > 0) this.dispatch("reconcile", { detail: { removed } });
|
|
313
393
|
}
|
|
314
|
-
/** Follows a `region` element swapped in — or arriving — at runtime (Turbo Stream). */
|
|
315
394
|
regionTargetConnected() {
|
|
316
395
|
this.#resync();
|
|
317
396
|
}
|
|
318
|
-
/** Releases the observation when the `region` element leaves the target set. */
|
|
319
397
|
regionTargetDisconnected() {
|
|
320
398
|
this.#resync();
|
|
321
399
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
* ownership through this one test; the observation gets it structurally, by watching
|
|
328
|
-
* that subtree and nothing else.
|
|
329
|
-
*/
|
|
400
|
+
maxValueChanged() {
|
|
401
|
+
if (!this.#connected) return;
|
|
402
|
+
this.#appliedMax = this.#safeMax;
|
|
403
|
+
this.#enforceMax();
|
|
404
|
+
}
|
|
330
405
|
#owns(message) {
|
|
331
406
|
return this.hasRegionTarget && this.regionTarget.contains(message);
|
|
332
407
|
}
|
|
333
|
-
/**
|
|
334
|
-
* Releases a message that left the target set (a Turbo Stream `remove`, the consumer
|
|
335
|
-
* detaching the node, or a morph that rewrote the target attribute in place): it
|
|
336
|
-
* stops occupying a `max` slot, and both its pending auto-dismiss and an already
|
|
337
|
-
* scheduled removal are cancelled. A move *within* the region keeps all of them —
|
|
338
|
-
* which is why the element must still be a message to be treated as one: ownership
|
|
339
|
-
* alone reads an in-place attribute rewrite as a move, and a node outside the target
|
|
340
|
-
* set belongs to the consumer, so nothing here may dismiss it.
|
|
341
|
-
*/
|
|
342
408
|
messageTargetDisconnected(message) {
|
|
343
409
|
if (!this.#connected) return;
|
|
344
|
-
const moved = this.#owns(message) && message.matches(
|
|
345
|
-
if (moved)
|
|
410
|
+
const moved = this.#owns(message) && message.matches(this.#messageSelector);
|
|
411
|
+
if (moved) {
|
|
412
|
+
this.#afterMove(message);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
346
415
|
this.#forget(message);
|
|
416
|
+
this.#arrivals.delete(message);
|
|
347
417
|
this.#leaving.delete(message);
|
|
348
418
|
}
|
|
349
|
-
/**
|
|
350
|
-
* Re-points the observation after a `region` swap and picks up the messages the
|
|
351
|
-
* new element brought with it (dynamic inserts, so their own `role` announces
|
|
352
|
-
* them). The `#connected` guard is load-bearing: Stimulus runs target callbacks
|
|
353
|
-
* for the initial markup *before* `connect()` and again during teardown *after*
|
|
354
|
-
* `disconnect()`, and re-observing there would outlive the controller.
|
|
355
|
-
*/
|
|
356
419
|
#resync() {
|
|
357
420
|
if (!this.#connected) return;
|
|
358
421
|
this.#syncObservation();
|
|
@@ -360,11 +423,6 @@ var FlashController = class extends Controller {
|
|
|
360
423
|
if (this.#owns(message)) this.#process(message, false);
|
|
361
424
|
}
|
|
362
425
|
}
|
|
363
|
-
/**
|
|
364
|
-
* Points the mutation observation at the current `region` target, re-resolved on
|
|
365
|
-
* every sync rather than captured at connect, so an element swapped in at runtime
|
|
366
|
-
* is observed instead of the detached original.
|
|
367
|
-
*/
|
|
368
426
|
#syncObservation() {
|
|
369
427
|
this.#stopObserving();
|
|
370
428
|
if (!this.hasRegionTarget || typeof MutationObserver === "undefined") return;
|
|
@@ -375,14 +433,7 @@ var FlashController = class extends Controller {
|
|
|
375
433
|
this.#observer?.disconnect();
|
|
376
434
|
this.#observer = null;
|
|
377
435
|
}
|
|
378
|
-
/**
|
|
379
|
-
* Pause-on-hover/focus listeners, bound and unbound as a pair so the two sides
|
|
380
|
-
* stay in sync. Binding is gated by `pauseOnHover`; unbinding is unconditional
|
|
381
|
-
* and idempotent (a no-op when nothing was bound), which keeps teardown correct
|
|
382
|
-
* even if `pauseOnHover` were ever toggled during a message's life.
|
|
383
|
-
*/
|
|
384
436
|
#bindPause(message) {
|
|
385
|
-
if (!this.pauseOnHoverValue) return;
|
|
386
437
|
message.addEventListener("mouseenter", this.#onEnter);
|
|
387
438
|
message.addEventListener("mouseleave", this.#onLeave);
|
|
388
439
|
message.addEventListener("focusin", this.#onEnter);
|
|
@@ -394,79 +445,128 @@ var FlashController = class extends Controller {
|
|
|
394
445
|
message.removeEventListener("focusin", this.#onEnter);
|
|
395
446
|
message.removeEventListener("focusout", this.#onLeave);
|
|
396
447
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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);
|
|
402
455
|
}
|
|
403
|
-
/** Processes messages added after connect (Turbo Stream); their own role announces them. */
|
|
404
456
|
#onMutations(mutations) {
|
|
405
457
|
for (const mutation of mutations) {
|
|
406
458
|
for (const node of mutation.addedNodes) {
|
|
407
459
|
if (!(node instanceof HTMLElement)) continue;
|
|
408
|
-
if (node.matches(
|
|
409
|
-
for (const message of node.querySelectorAll(
|
|
460
|
+
if (node.matches(this.#messageSelector)) this.#process(node, false);
|
|
461
|
+
for (const message of node.querySelectorAll(this.#messageSelector)) {
|
|
410
462
|
this.#process(message, false);
|
|
411
463
|
}
|
|
412
464
|
}
|
|
413
465
|
}
|
|
414
466
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
*/
|
|
420
|
-
#process(message, bridge) {
|
|
421
|
-
if (this.#order.includes(message) || this.#leaving.has(message)) return;
|
|
467
|
+
#process(message, atConnect) {
|
|
468
|
+
if (this.#order.includes(message) || this.#leaving.has(message) || !this.#owns(message)) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
422
471
|
const type = message.getAttribute("data-flash-type") ?? "";
|
|
423
472
|
const assertive = ASSERTIVE_TYPES.has(type);
|
|
424
473
|
if (!message.hasAttribute("role")) {
|
|
425
474
|
message.setAttribute("role", assertive ? "alert" : "status");
|
|
426
475
|
}
|
|
427
476
|
message.setAttribute("data-flash-state", "visible");
|
|
477
|
+
message.setAttribute(this.#shownAttribute, "");
|
|
478
|
+
const known = this.#arrivals.has(message);
|
|
428
479
|
this.#order.push(message);
|
|
480
|
+
this.#arrivals.add(message);
|
|
429
481
|
this.#bindPause(message);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
482
|
+
this.#readHolds(message);
|
|
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
|
+
}
|
|
433
488
|
this.#startTimer(message);
|
|
434
|
-
this.#enforceMax();
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
489
|
+
if (!atConnect) this.#enforceMax([message]);
|
|
490
|
+
}
|
|
491
|
+
#enforceMax(spared = []) {
|
|
492
|
+
if (this.#safeMax <= 0) return;
|
|
493
|
+
for (const message of [...this.#arrivals]) {
|
|
494
|
+
if (this.#shownCount() <= this.#safeMax) return;
|
|
495
|
+
if (this.#owns(message) && !spared.includes(message) && !this.#dismiss.isHeld(message)) {
|
|
496
|
+
this.#beginDismiss(message, "limit");
|
|
497
|
+
}
|
|
443
498
|
}
|
|
444
499
|
}
|
|
445
|
-
|
|
500
|
+
#shownCount() {
|
|
501
|
+
return this.#order.filter((message) => this.#owns(message)).length;
|
|
502
|
+
}
|
|
446
503
|
#startTimer(message) {
|
|
447
|
-
if (this
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
* (WCAG 2.2 2.2.1). Hover and focus are independent reasons, and the registry
|
|
453
|
-
* banks the time left on the first of them and waits for the last.
|
|
454
|
-
*/
|
|
504
|
+
if (this.#safeDuration <= 0) return;
|
|
505
|
+
const dismiss = () => this.#beginDismiss(message, "timeout");
|
|
506
|
+
if (this.pauseOnHoverValue) this.#dismiss.set(message, dismiss, this.#safeDuration);
|
|
507
|
+
else this.#fixedDismiss.set(message, dismiss, this.#safeDuration);
|
|
508
|
+
}
|
|
455
509
|
#pause(message, reason) {
|
|
456
510
|
this.#dismiss.pause(message, reason);
|
|
457
511
|
}
|
|
458
|
-
|
|
459
|
-
|
|
512
|
+
#resume(message, reason, spare) {
|
|
513
|
+
const held = this.#dismiss.isHeld(message);
|
|
460
514
|
this.#dismiss.resume(message, reason);
|
|
461
|
-
|
|
462
|
-
|
|
515
|
+
if (held && !this.#dismiss.isHeld(message)) this.#capLater(spare);
|
|
516
|
+
}
|
|
517
|
+
#capLater(spare) {
|
|
518
|
+
if (spare) this.#spared.push(spare);
|
|
519
|
+
this.#reapply.schedule();
|
|
520
|
+
}
|
|
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
|
+
}
|
|
531
|
+
const spared = this.#spared;
|
|
532
|
+
this.#spared = [];
|
|
533
|
+
this.#enforceMax(spared);
|
|
534
|
+
}
|
|
535
|
+
#afterMove(message) {
|
|
536
|
+
if (!message.contains(document.activeElement)) this.#resume(message, "focus", null);
|
|
537
|
+
if (this.#dismiss.isHeld(message)) this.#awaitPointer(message);
|
|
538
|
+
}
|
|
539
|
+
#readHolds(message) {
|
|
540
|
+
if (message.contains(document.activeElement)) this.#pause(message, "focus");
|
|
541
|
+
if (!message.matches(":hover")) return;
|
|
542
|
+
this.#pause(message, "hover");
|
|
543
|
+
this.#awaitPointer(message);
|
|
544
|
+
}
|
|
545
|
+
#awaitPointer(message) {
|
|
546
|
+
this.#awaitingPointer.add(message);
|
|
547
|
+
this.#pointer.add(document, "pointermove", this.#onPointerMove, {
|
|
548
|
+
capture: true,
|
|
549
|
+
passive: true
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
#onPointerMove = () => {
|
|
553
|
+
this.#pointer.dispose();
|
|
554
|
+
const waiting = [...this.#awaitingPointer];
|
|
555
|
+
this.#awaitingPointer.clear();
|
|
556
|
+
for (const message of waiting) {
|
|
557
|
+
if (!message.matches(":hover")) this.#resume(message, "hover", null);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
463
560
|
#forget(message) {
|
|
561
|
+
if (this.#dismiss.isHeld(message)) this.#capLater(null);
|
|
464
562
|
this.#dismiss.clear(message);
|
|
563
|
+
this.#fixedDismiss.clear(message);
|
|
465
564
|
const index = this.#order.indexOf(message);
|
|
466
565
|
if (index !== -1) this.#order.splice(index, 1);
|
|
467
566
|
this.#unbindPause(message);
|
|
567
|
+
this.#awaitingPointer.delete(message);
|
|
568
|
+
if (this.#awaitingPointer.size === 0) this.#pointer.dispose();
|
|
468
569
|
}
|
|
469
|
-
/** Marks a message leaving, then removes it after its CSS transition and emits dismiss. */
|
|
470
570
|
#beginDismiss(message, reason) {
|
|
471
571
|
if (!this.#order.includes(message)) return;
|
|
472
572
|
this.#forget(message);
|
|
@@ -475,6 +575,7 @@ var FlashController = class extends Controller {
|
|
|
475
575
|
const finalize = () => {
|
|
476
576
|
if (!this.#leaving.delete(message)) return;
|
|
477
577
|
message.remove();
|
|
578
|
+
this.#arrivals.delete(message);
|
|
478
579
|
this.dispatch("dismiss", { detail: { element: message, reason } });
|
|
479
580
|
};
|
|
480
581
|
const transition = this.#transitionMs(message);
|
|
@@ -484,13 +585,29 @@ var FlashController = class extends Controller {
|
|
|
484
585
|
finalize();
|
|
485
586
|
}
|
|
486
587
|
}
|
|
487
|
-
/** Maximum transition total (duration + delay) of `el` in ms (0 when none / unsupported). */
|
|
488
588
|
#transitionMs(el) {
|
|
489
589
|
if (typeof window.getComputedStyle !== "function") return 0;
|
|
490
590
|
return maxTransitionTotalMs(window.getComputedStyle(el));
|
|
491
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
|
+
}
|
|
492
610
|
};
|
|
493
611
|
|
|
494
612
|
export { FlashController };
|
|
495
|
-
//# sourceMappingURL=flash_controller.js.map
|
|
496
613
|
//# sourceMappingURL=flash_controller.js.map
|