stimeo-ui 0.14.0 → 0.16.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 +212 -0
- data/README.md +120 -0
- data/dist/cable/index.js +123 -29
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +96 -62
- data/dist/controllers/auto_submit_controller.js +83 -7
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +38 -11
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +340 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +81 -12
- data/dist/controllers/clipboard_controller.js +63 -5
- data/dist/controllers/collapsible_controller.js +99 -14
- data/dist/controllers/color_picker_controller.js +80 -34
- data/dist/controllers/combobox_controller.js +106 -16
- data/dist/controllers/command_palette_controller.js +35 -3
- data/dist/controllers/conditional_fields_controller.js +85 -17
- data/dist/controllers/confirm_controller.js +3 -0
- data/dist/controllers/context_menu_controller.js +32 -12
- data/dist/controllers/countdown_controller.js +129 -26
- data/dist/controllers/currency_input_controller.js +221 -67
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +151 -30
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +46 -13
- data/dist/controllers/dismissible_controller.js +1 -0
- data/dist/controllers/drawer_controller.js +54 -19
- data/dist/controllers/dropdown_controller.js +36 -9
- data/dist/controllers/editable_controller.js +34 -0
- data/dist/controllers/file_dropzone_controller.js +144 -51
- data/dist/controllers/filter_controller.js +20 -6
- data/dist/controllers/flash_controller.js +432 -71
- data/dist/controllers/focus_controller.js +1 -0
- data/dist/controllers/form_field_controller.js +7 -5
- data/dist/controllers/form_validation_controller.js +19 -13
- data/dist/controllers/frame_loading_controller.js +45 -8
- data/dist/controllers/highlight_controller.js +82 -25
- data/dist/controllers/hover_card_controller.js +40 -14
- data/dist/controllers/idle_controller.js +90 -5
- data/dist/controllers/input_mask_controller.js +65 -9
- data/dist/controllers/intersection_controller.js +3 -0
- data/dist/controllers/lazy_frame_controller.js +11 -2
- data/dist/controllers/listbox_controller.js +203 -45
- data/dist/controllers/local_time_controller.js +10 -5
- data/dist/controllers/masonry_controller.js +31 -15
- data/dist/controllers/menu_controller.js +45 -16
- data/dist/controllers/menubar_controller.js +58 -24
- data/dist/controllers/meter_controller.js +9 -5
- data/dist/controllers/multi_select_controller.js +278 -104
- data/dist/controllers/navigation_menu_controller.js +48 -15
- data/dist/controllers/nested_form_controller.js +37 -8
- data/dist/controllers/network_status_controller.js +9 -1
- data/dist/controllers/number_input_controller.js +124 -21
- data/dist/controllers/optimistic_controller.js +42 -5
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +115 -21
- data/dist/controllers/overflow_menu_controller.js +141 -44
- data/dist/controllers/pagination_controller.js +74 -28
- data/dist/controllers/password_reveal_controller.js +59 -2
- data/dist/controllers/persist_controller.js +30 -8
- data/dist/controllers/pointer_drag_controller.js +131 -52
- data/dist/controllers/popover_controller.js +45 -11
- data/dist/controllers/portal_controller.js +6 -2
- data/dist/controllers/preview_guard_controller.js +16 -1
- data/dist/controllers/progress_controller.js +8 -4
- data/dist/controllers/radio_group_controller.js +42 -17
- data/dist/controllers/range_slider_controller.js +88 -42
- data/dist/controllers/rating_controller.js +39 -15
- data/dist/controllers/read_more_controller.js +100 -7
- data/dist/controllers/reading_progress_controller.js +65 -19
- data/dist/controllers/relative_time_controller.js +10 -5
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +75 -27
- data/dist/controllers/scroll_restore_controller.js +37 -16
- data/dist/controllers/scroll_visibility_controller.js +49 -30
- data/dist/controllers/scrollspy_controller.js +71 -26
- data/dist/controllers/separator_controller.js +66 -37
- data/dist/controllers/sidebar_controller.js +77 -18
- data/dist/controllers/skeleton_controller.js +6 -1
- data/dist/controllers/slider_controller.js +82 -47
- data/dist/controllers/smart_sticky_header_controller.js +60 -26
- data/dist/controllers/sortable_controller.js +17 -2
- data/dist/controllers/spinner_controller.js +10 -2
- data/dist/controllers/step_indicator_controller.js +18 -17
- data/dist/controllers/stepper_controller.js +101 -19
- data/dist/controllers/stick_to_bottom_controller.js +104 -8
- data/dist/controllers/submit_once_controller.js +45 -9
- data/dist/controllers/switch_controller.js +101 -10
- data/dist/controllers/tabs_controller.js +21 -2
- data/dist/controllers/tags_input_controller.js +209 -59
- data/dist/controllers/textarea_autosize_controller.js +29 -3
- data/dist/controllers/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +451 -105
- data/dist/controllers/toggle_group_controller.js +159 -23
- data/dist/controllers/toolbar_controller.js +32 -0
- data/dist/controllers/tooltip_controller.js +39 -13
- data/dist/controllers/transition_controller.js +4 -0
- data/dist/controllers/tree_view_controller.js +169 -16
- data/dist/index.js +5002 -1911
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -96,13 +96,19 @@ var CONSTRAINT_MESSAGE_KEYS = [
|
|
|
96
96
|
["stepMismatch", "step-mismatch"],
|
|
97
97
|
["badInput", "bad-input"]
|
|
98
98
|
];
|
|
99
|
-
var
|
|
100
|
-
var
|
|
101
|
-
var DISALLOW_ATTR = "data-stimeo--form-validation-disallow";
|
|
102
|
-
var DISALLOW_WHITESPACE_MESSAGE = `${MESSAGE_ATTR_PREFIX}whitespace`;
|
|
99
|
+
var MESSAGE_PART = "message";
|
|
100
|
+
var DISALLOW_PART = "disallow";
|
|
103
101
|
var DISALLOW_WHITESPACE_DEFAULT = "Please enter a value that is not only whitespace.";
|
|
104
102
|
var FORM_FIELD_INVALID_ATTR = "data-stimeo--form-field-invalid";
|
|
105
103
|
var FormValidationController = class _FormValidationController extends Controller {
|
|
104
|
+
/** The generic message override, in the namespace this controller is registered under. */
|
|
105
|
+
get #messageAttribute() {
|
|
106
|
+
return `data-${this.identifier}-${MESSAGE_PART}`;
|
|
107
|
+
}
|
|
108
|
+
/** Marks the `novalidate` this controller applied, so an authored one is left alone. */
|
|
109
|
+
get #novalidateMarker() {
|
|
110
|
+
return `data-${this.identifier}-${_FormValidationController.#NOVALIDATE_PART}`;
|
|
111
|
+
}
|
|
106
112
|
static outlets = ["stimeo--form-field"];
|
|
107
113
|
static values = {
|
|
108
114
|
validateOnBlur: { type: Boolean, default: true },
|
|
@@ -112,8 +118,8 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
112
118
|
};
|
|
113
119
|
static actions = ["validate"];
|
|
114
120
|
static events = ["valid", "invalid"];
|
|
115
|
-
/**
|
|
116
|
-
static #
|
|
121
|
+
/** Suffix of the marker recording that we added `novalidate`, so we only remove our own. */
|
|
122
|
+
static #NOVALIDATE_PART = "novalidate";
|
|
117
123
|
/** Object-backed groups already interacted with — the input revalidation gate. */
|
|
118
124
|
#touchedGroups = /* @__PURE__ */ new WeakSet();
|
|
119
125
|
/** String-backed fallback radio groups already interacted with. */
|
|
@@ -169,7 +175,7 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
169
175
|
/** Suppresses native bubbles and binds the submit / blur / input listeners. */
|
|
170
176
|
connect() {
|
|
171
177
|
if (setDefaultAttribute(this.element, "novalidate", "")) {
|
|
172
|
-
this.element.setAttribute(
|
|
178
|
+
this.element.setAttribute(this.#novalidateMarker, "");
|
|
173
179
|
}
|
|
174
180
|
this.#listenerDocument = this.element.ownerDocument;
|
|
175
181
|
this.#listenerDocument.addEventListener("submit", this.#onSubmit, true);
|
|
@@ -189,9 +195,9 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
189
195
|
}
|
|
190
196
|
this.#ownedCustomErrors.clear();
|
|
191
197
|
this.#touchedRadioGroups.clear();
|
|
192
|
-
if (this.element.hasAttribute(
|
|
198
|
+
if (this.element.hasAttribute(this.#novalidateMarker)) {
|
|
193
199
|
this.element.removeAttribute("novalidate");
|
|
194
|
-
this.element.removeAttribute(
|
|
200
|
+
this.element.removeAttribute(this.#novalidateMarker);
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
203
|
/**
|
|
@@ -283,10 +289,10 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
283
289
|
if (control.validity.customError) return control.validationMessage;
|
|
284
290
|
for (const [flag, key] of CONSTRAINT_MESSAGE_KEYS) {
|
|
285
291
|
if (control.validity[flag]) {
|
|
286
|
-
return this.#authoredMessage(control, `${
|
|
292
|
+
return this.#authoredMessage(control, `${this.#messageAttribute}-${key}`) ?? this.#authoredMessage(control, this.#messageAttribute) ?? control.validationMessage;
|
|
287
293
|
}
|
|
288
294
|
}
|
|
289
|
-
return control.validationMessage || this.#authoredMessage(control,
|
|
295
|
+
return control.validationMessage || this.#authoredMessage(control, this.#messageAttribute) || "";
|
|
290
296
|
}
|
|
291
297
|
/**
|
|
292
298
|
* Applies (or clears) a declarative custom constraint via `setCustomValidity`,
|
|
@@ -302,14 +308,14 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
302
308
|
#syncCustomValidity(control) {
|
|
303
309
|
const recorded = this.#ownedCustomErrors.get(control);
|
|
304
310
|
const ownsCurrent = recorded !== void 0 && control.validationMessage === recorded;
|
|
305
|
-
const violates = control.getAttribute(
|
|
311
|
+
const violates = control.getAttribute(`data-${this.identifier}-${DISALLOW_PART}`) === "whitespace" && control.value.length > 0 && control.value.trim() === "";
|
|
306
312
|
if (!violates) {
|
|
307
313
|
if (ownsCurrent) control.setCustomValidity("");
|
|
308
314
|
this.#ownedCustomErrors.delete(control);
|
|
309
315
|
return;
|
|
310
316
|
}
|
|
311
317
|
if (control.validity.customError && !ownsCurrent) return;
|
|
312
|
-
const message = this.#authoredMessage(control,
|
|
318
|
+
const message = this.#authoredMessage(control, `${this.#messageAttribute}-whitespace`) ?? this.#authoredMessage(control, this.#messageAttribute) ?? DISALLOW_WHITESPACE_DEFAULT;
|
|
313
319
|
if (!ownsCurrent || recorded !== message) control.setCustomValidity(message);
|
|
314
320
|
this.#ownedCustomErrors.set(control, message);
|
|
315
321
|
}
|
|
@@ -103,6 +103,30 @@ var DetachGate = class _DetachGate {
|
|
|
103
103
|
}
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
// src/utils/listener_set.ts
|
|
107
|
+
var ListenerSet = class {
|
|
108
|
+
/** The generation every `add` joins until the next `dispose()`. */
|
|
109
|
+
#abort = new AbortController();
|
|
110
|
+
/**
|
|
111
|
+
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
112
|
+
*
|
|
113
|
+
* The set supplies the signal, so `options` carries everything else the DOM
|
|
114
|
+
* accepts — `capture` included, which has to match at release time and no
|
|
115
|
+
* longer has a second place to drift from.
|
|
116
|
+
*/
|
|
117
|
+
add(target, type, handler, options) {
|
|
118
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Releases every listener of the open generation, synchronously, and opens the
|
|
122
|
+
* next one. Idempotent, and safe before anything has been added.
|
|
123
|
+
*/
|
|
124
|
+
dispose() {
|
|
125
|
+
this.#abort.abort();
|
|
126
|
+
this.#abort = new AbortController();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
106
130
|
// src/utils/min_duration_floor.ts
|
|
107
131
|
var MinDurationFloor = class {
|
|
108
132
|
#timers;
|
|
@@ -218,6 +242,7 @@ var FrameLoadingController = class extends Controller {
|
|
|
218
242
|
#floor = new MinDurationFloor(this.#timeouts);
|
|
219
243
|
#gate = new DetachGate();
|
|
220
244
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
245
|
+
#listeners = new ListenerSet();
|
|
221
246
|
#loading = false;
|
|
222
247
|
/**
|
|
223
248
|
* The optional targets this controller revealed, and the content it marked inert.
|
|
@@ -237,6 +262,10 @@ var FrameLoadingController = class extends Controller {
|
|
|
237
262
|
this.#floor.cancel();
|
|
238
263
|
if (!this.#loading) this.#begin();
|
|
239
264
|
};
|
|
265
|
+
/**
|
|
266
|
+
* @stimeoRuntimeOnly `minDuration` holds the loading state at least this long for the one load
|
|
267
|
+
* that ended.
|
|
268
|
+
*/
|
|
240
269
|
#onEnd = () => {
|
|
241
270
|
if (!this.#loading) return;
|
|
242
271
|
this.#floor.schedule(this.minDurationValue, () => this.#finish());
|
|
@@ -244,14 +273,12 @@ var FrameLoadingController = class extends Controller {
|
|
|
244
273
|
connect() {
|
|
245
274
|
this.#gate.cancel();
|
|
246
275
|
this.#beforeCache.activate();
|
|
247
|
-
this.element
|
|
248
|
-
this.element
|
|
249
|
-
this.element
|
|
276
|
+
this.#listeners.add(this.element, "turbo:before-fetch-request", this.#onStart);
|
|
277
|
+
this.#listeners.add(this.element, "turbo:frame-load", this.#onEnd);
|
|
278
|
+
this.#listeners.add(this.element, "turbo:fetch-request-error", this.#onEnd);
|
|
250
279
|
}
|
|
251
280
|
disconnect() {
|
|
252
|
-
this.
|
|
253
|
-
this.element.removeEventListener("turbo:frame-load", this.#onEnd);
|
|
254
|
-
this.element.removeEventListener("turbo:fetch-request-error", this.#onEnd);
|
|
281
|
+
this.#listeners.dispose();
|
|
255
282
|
this.#beforeCache.deactivate();
|
|
256
283
|
this.#gate.disconnected(this, () => this.#teardown());
|
|
257
284
|
}
|
|
@@ -340,7 +367,12 @@ var FrameLoadingController = class extends Controller {
|
|
|
340
367
|
this.#revealedOverlay = this.overlayTarget;
|
|
341
368
|
this.overlayTarget.hidden = false;
|
|
342
369
|
}
|
|
343
|
-
/**
|
|
370
|
+
/**
|
|
371
|
+
* Enters the loading state: hooks, skeleton/overlay, inert content, focus retreat.
|
|
372
|
+
*
|
|
373
|
+
* @stimeoRuntimeOnly `announceText` words the one announcement of this load and `restoreFocus`
|
|
374
|
+
* decides whether focus leaves the frame it covers.
|
|
375
|
+
*/
|
|
344
376
|
#begin() {
|
|
345
377
|
this.#loading = true;
|
|
346
378
|
this.#floor.begin();
|
|
@@ -353,7 +385,12 @@ var FrameLoadingController = class extends Controller {
|
|
|
353
385
|
this.dispatch("start", { detail: {} });
|
|
354
386
|
announce(fillTemplate(this.announceTextValue, {}));
|
|
355
387
|
}
|
|
356
|
-
/**
|
|
388
|
+
/**
|
|
389
|
+
* Leaves the loading state: restore hooks, hide skeleton/overlay, restore focus.
|
|
390
|
+
*
|
|
391
|
+
* @stimeoRuntimeOnly `announceReadyText` words the one announcement of this finished load and
|
|
392
|
+
* `restoreFocus` decides whether focus goes back.
|
|
393
|
+
*/
|
|
357
394
|
#finish() {
|
|
358
395
|
this.#loading = false;
|
|
359
396
|
this.#rewindHooks();
|
|
@@ -2,11 +2,6 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/highlight_controller.ts
|
|
4
4
|
|
|
5
|
-
// src/utils/reduced_motion.ts
|
|
6
|
-
function prefersReducedMotion() {
|
|
7
|
-
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
5
|
// src/utils/safe_timeout.ts
|
|
11
6
|
var TimerRegistry = class {
|
|
12
7
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -60,7 +55,68 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
60
55
|
}
|
|
61
56
|
};
|
|
62
57
|
|
|
58
|
+
// src/utils/keyed_timers.ts
|
|
59
|
+
var KeyedTimers = class {
|
|
60
|
+
#timers = new SafeTimeout();
|
|
61
|
+
/** The pending timer of each key; an entry lives exactly as long as its timer. */
|
|
62
|
+
#pending = /* @__PURE__ */ new Map();
|
|
63
|
+
/**
|
|
64
|
+
* Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had
|
|
65
|
+
* pending. The entry is dropped before the callback runs, so the callback sees
|
|
66
|
+
* the key unarmed and may arm it again for the next round.
|
|
67
|
+
*/
|
|
68
|
+
set(key, callback, delay) {
|
|
69
|
+
this.clear(key);
|
|
70
|
+
const id = this.#timers.set(() => {
|
|
71
|
+
this.#pending.delete(key);
|
|
72
|
+
callback();
|
|
73
|
+
}, delay);
|
|
74
|
+
this.#pending.set(key, id);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Cancels `key`'s pending timer, if it has one.
|
|
78
|
+
*
|
|
79
|
+
* A timer id is a positive integer, so `-1` stands for "nothing pending" and
|
|
80
|
+
* the registry ignores an id it does not own — the unarmed case needs no
|
|
81
|
+
* branch of its own, and no other key's timer can be reached from here.
|
|
82
|
+
*/
|
|
83
|
+
clear(key) {
|
|
84
|
+
this.#timers.clear(this.#pending.get(key) ?? -1);
|
|
85
|
+
this.#pending.delete(key);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Cancels every pending timer and forgets every key. Call this from a
|
|
89
|
+
* controller's `disconnect()` so no timer, and no entry, outlives the element.
|
|
90
|
+
*/
|
|
91
|
+
clearAll() {
|
|
92
|
+
this.#timers.clearAll();
|
|
93
|
+
this.#pending.clear();
|
|
94
|
+
}
|
|
95
|
+
/** Whether `key` has a timer pending. */
|
|
96
|
+
has(key) {
|
|
97
|
+
return this.#pending.has(key);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// src/utils/reduced_motion.ts
|
|
102
|
+
function prefersReducedMotion() {
|
|
103
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/utils/transient_hooks.ts
|
|
107
|
+
var TransientHooks = class {
|
|
108
|
+
#attributes;
|
|
109
|
+
constructor(options) {
|
|
110
|
+
this.#attributes = options.attributes;
|
|
111
|
+
}
|
|
112
|
+
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
113
|
+
reset(element) {
|
|
114
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
63
118
|
// src/controllers/highlight_controller.ts
|
|
119
|
+
var TRANSIENT = new TransientHooks({ attributes: ["data-highlight"] });
|
|
64
120
|
var hookOwners = /* @__PURE__ */ new WeakMap();
|
|
65
121
|
var HighlightController = class extends Controller {
|
|
66
122
|
static values = {
|
|
@@ -68,14 +124,11 @@ var HighlightController = class extends Controller {
|
|
|
68
124
|
observe: { type: Boolean, default: false }
|
|
69
125
|
};
|
|
70
126
|
static events = ["start", "end"];
|
|
71
|
-
#timeouts = new SafeTimeout();
|
|
72
127
|
/**
|
|
73
|
-
* The removal timer this connection has outstanding for
|
|
74
|
-
*
|
|
75
|
-
* so a cleared id can never be matched against a recycled one. Which connection owns
|
|
76
|
-
* an element's hook is answered by the shared owner registry above.
|
|
128
|
+
* The removal timer this connection has outstanding for each element. Which
|
|
129
|
+
* connection owns an element's hook is answered by the shared owner registry above.
|
|
77
130
|
*/
|
|
78
|
-
#
|
|
131
|
+
#removal = new KeyedTimers();
|
|
79
132
|
#observer = null;
|
|
80
133
|
connect() {
|
|
81
134
|
this.#clearArrivedHook(this.element);
|
|
@@ -92,13 +145,12 @@ var HighlightController = class extends Controller {
|
|
|
92
145
|
disconnect() {
|
|
93
146
|
this.#observer?.disconnect();
|
|
94
147
|
this.#observer = null;
|
|
95
|
-
this.#
|
|
96
|
-
this.#pending = /* @__PURE__ */ new WeakMap();
|
|
148
|
+
this.#removal.clearAll();
|
|
97
149
|
}
|
|
98
150
|
/** Drops a hook that arrived with the DOM, along with this connection's claim on it. */
|
|
99
151
|
#clearArrivedHook(el) {
|
|
100
152
|
if (hookOwners.get(el) === this) hookOwners.delete(el);
|
|
101
|
-
|
|
153
|
+
TRANSIENT.reset(el);
|
|
102
154
|
}
|
|
103
155
|
/** Highlights every element child added by a childList mutation. */
|
|
104
156
|
#onMutations(mutations) {
|
|
@@ -108,19 +160,25 @@ var HighlightController = class extends Controller {
|
|
|
108
160
|
}
|
|
109
161
|
}
|
|
110
162
|
}
|
|
111
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* Flags `el` with `data-highlight` and schedules its removal (unless reduced-motion).
|
|
165
|
+
*
|
|
166
|
+
* @stimeoRuntimeOnly `duration` is the length of the one emphasis this call arms.
|
|
167
|
+
*/
|
|
112
168
|
#highlight(el) {
|
|
113
169
|
if (prefersReducedMotion()) return;
|
|
114
170
|
this.#releasePending(el);
|
|
115
171
|
el.setAttribute("data-highlight", "true");
|
|
116
172
|
this.dispatch("start", { target: el, detail: { element: el } });
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
173
|
+
this.#removal.set(
|
|
174
|
+
el,
|
|
175
|
+
() => {
|
|
176
|
+
hookOwners.delete(el);
|
|
177
|
+
el.removeAttribute("data-highlight");
|
|
178
|
+
this.dispatch("end", { target: el, detail: { element: el } });
|
|
179
|
+
},
|
|
180
|
+
this.durationValue
|
|
181
|
+
);
|
|
124
182
|
hookOwners.set(el, this);
|
|
125
183
|
}
|
|
126
184
|
/**
|
|
@@ -133,10 +191,9 @@ var HighlightController = class extends Controller {
|
|
|
133
191
|
if (owner !== void 0 && owner !== this) owner.#cancelPending(el);
|
|
134
192
|
this.#cancelPending(el);
|
|
135
193
|
}
|
|
136
|
-
/** Releases `el`'s pending removal timer, if it has one. */
|
|
194
|
+
/** Releases `el`'s pending removal timer, if it has one, and this connection's claim. */
|
|
137
195
|
#cancelPending(el) {
|
|
138
|
-
this.#
|
|
139
|
-
this.#pending.delete(el);
|
|
196
|
+
this.#removal.clear(el);
|
|
140
197
|
hookOwners.delete(el);
|
|
141
198
|
}
|
|
142
199
|
};
|
|
@@ -164,6 +164,16 @@ function isScrollable(element) {
|
|
|
164
164
|
return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// src/utils/state_reason.ts
|
|
168
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
169
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
170
|
+
function stateReasonFor(event) {
|
|
171
|
+
if (!event) return "api";
|
|
172
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
173
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
174
|
+
return "user";
|
|
175
|
+
}
|
|
176
|
+
|
|
167
177
|
// src/controllers/hover_card_controller.ts
|
|
168
178
|
var HoverCardController = class extends Controller {
|
|
169
179
|
static targets = ["trigger", "card"];
|
|
@@ -173,6 +183,7 @@ var HoverCardController = class extends Controller {
|
|
|
173
183
|
closeOnScroll: { type: Boolean, default: false }
|
|
174
184
|
};
|
|
175
185
|
static actions = ["close", "open"];
|
|
186
|
+
static events = ["close", "open"];
|
|
176
187
|
/** Pending open/close timers, with their IDs reset on every lifecycle boundary. */
|
|
177
188
|
#timers = new SafeTimeout();
|
|
178
189
|
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
@@ -181,14 +192,18 @@ var HoverCardController = class extends Controller {
|
|
|
181
192
|
#pendingClose = null;
|
|
182
193
|
/** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */
|
|
183
194
|
#stopScrollDismiss = null;
|
|
195
|
+
/** Whether state moves are reported: set once `connect()` settled the baseline. */
|
|
196
|
+
#reporting = false;
|
|
184
197
|
/** Starts closed and discards any stale pending state from a prior connection. */
|
|
185
198
|
connect() {
|
|
186
199
|
this.#cancelOpen();
|
|
187
200
|
this.#cancelClose();
|
|
188
|
-
this.#conceal();
|
|
201
|
+
this.#conceal("api");
|
|
202
|
+
this.#reporting = true;
|
|
189
203
|
}
|
|
190
204
|
/** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */
|
|
191
205
|
disconnect() {
|
|
206
|
+
this.#reporting = false;
|
|
192
207
|
this.#cancelOpen();
|
|
193
208
|
this.#cancelClose();
|
|
194
209
|
this.#escapeLayer.deactivate();
|
|
@@ -196,16 +211,17 @@ var HoverCardController = class extends Controller {
|
|
|
196
211
|
this.#stopScrollDismiss = null;
|
|
197
212
|
}
|
|
198
213
|
/** Opens the card, after `openDelay` ms (or immediately at 0). Cancels a pending close. */
|
|
199
|
-
open() {
|
|
214
|
+
open(event) {
|
|
215
|
+
const reason = stateReasonFor(event);
|
|
200
216
|
this.#cancelClose();
|
|
201
217
|
if (this.#isOpen || this.#pendingOpen !== null) return;
|
|
202
218
|
if (this.openDelayValue <= 0) {
|
|
203
|
-
this.#reveal();
|
|
219
|
+
this.#reveal(reason);
|
|
204
220
|
return;
|
|
205
221
|
}
|
|
206
222
|
this.#pendingOpen = this.#timers.set(() => {
|
|
207
223
|
this.#pendingOpen = null;
|
|
208
|
-
this.#reveal();
|
|
224
|
+
this.#reveal(reason);
|
|
209
225
|
}, this.openDelayValue);
|
|
210
226
|
}
|
|
211
227
|
/**
|
|
@@ -214,28 +230,37 @@ var HoverCardController = class extends Controller {
|
|
|
214
230
|
* (e.g. a link in the card) and, if so, aborts the close — covering keyboard
|
|
215
231
|
* traversal that the pointer-only hoverable bridge cannot.
|
|
216
232
|
*/
|
|
217
|
-
close() {
|
|
233
|
+
close(event) {
|
|
234
|
+
const reason = stateReasonFor(event);
|
|
218
235
|
this.#cancelOpen();
|
|
219
236
|
if (!this.#isOpen || this.#pendingClose !== null) return;
|
|
220
237
|
this.#pendingClose = this.#timers.set(() => {
|
|
221
238
|
this.#pendingClose = null;
|
|
222
239
|
if (this.element.contains(document.activeElement)) return;
|
|
223
|
-
this.#conceal();
|
|
240
|
+
this.#conceal(reason);
|
|
224
241
|
}, this.closeDelayValue);
|
|
225
242
|
}
|
|
226
|
-
/**
|
|
227
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Reveals the card, reflects state, reports a move, and joins the Escape stack.
|
|
245
|
+
*
|
|
246
|
+
* @stimeoRuntimeOnly `closeOnScroll` decides whether this reveal wires the scroll dismissal; what
|
|
247
|
+
* is shown does not depend on it.
|
|
248
|
+
*/
|
|
249
|
+
#reveal(reason) {
|
|
228
250
|
if (!this.hasCardTarget) return;
|
|
251
|
+
const was = this.#isOpen;
|
|
229
252
|
this.cardTarget.hidden = false;
|
|
230
253
|
this.cardTarget.setAttribute("data-state", "open");
|
|
231
254
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "true");
|
|
232
|
-
this.#
|
|
255
|
+
if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
256
|
+
this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss("escape") });
|
|
233
257
|
if (this.closeOnScrollValue && !this.#stopScrollDismiss) {
|
|
234
|
-
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss());
|
|
258
|
+
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss("scroll"));
|
|
235
259
|
}
|
|
236
260
|
}
|
|
237
|
-
/** Hides the card, reflects state, and leaves the Escape stack
|
|
238
|
-
#conceal() {
|
|
261
|
+
/** Hides the card, reflects state, reports a move, and leaves the Escape stack. */
|
|
262
|
+
#conceal(reason) {
|
|
263
|
+
const was = this.#isOpen;
|
|
239
264
|
this.#escapeLayer.deactivate();
|
|
240
265
|
this.#stopScrollDismiss?.();
|
|
241
266
|
this.#stopScrollDismiss = null;
|
|
@@ -244,12 +269,13 @@ var HoverCardController = class extends Controller {
|
|
|
244
269
|
this.cardTarget.setAttribute("data-state", "closed");
|
|
245
270
|
}
|
|
246
271
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "false");
|
|
272
|
+
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
247
273
|
}
|
|
248
274
|
/** Cancels pending timers and conceals immediately (shared Escape path). */
|
|
249
|
-
#dismiss() {
|
|
275
|
+
#dismiss(reason) {
|
|
250
276
|
this.#cancelOpen();
|
|
251
277
|
this.#cancelClose();
|
|
252
|
-
this.#conceal();
|
|
278
|
+
this.#conceal(reason);
|
|
253
279
|
}
|
|
254
280
|
/** Cancels any pending open timer. */
|
|
255
281
|
#cancelOpen() {
|
|
@@ -55,6 +55,47 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
// src/utils/state_regions.ts
|
|
59
|
+
var StateRegions = class {
|
|
60
|
+
#whenTrue;
|
|
61
|
+
#whenFalse;
|
|
62
|
+
#taken = /* @__PURE__ */ new WeakSet();
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this.#whenTrue = options.whenTrue;
|
|
65
|
+
this.#whenFalse = options.whenFalse ?? null;
|
|
66
|
+
}
|
|
67
|
+
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
68
|
+
reflect(host, isTrue) {
|
|
69
|
+
const shown = this.#inside(host, this.#whenTrue());
|
|
70
|
+
if (!this.#whenFalse) {
|
|
71
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const hiddenSide = this.#inside(host, this.#whenFalse());
|
|
75
|
+
if (shown.length === 0 || hiddenSide.length === 0) {
|
|
76
|
+
for (const region of shown) this.#give(region);
|
|
77
|
+
for (const region of hiddenSide) this.#give(region);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
81
|
+
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
82
|
+
}
|
|
83
|
+
/** Writes `hidden` where it moves, noting which regions it takes out of view. */
|
|
84
|
+
#write(region, hidden) {
|
|
85
|
+
if (hidden) this.#taken.add(region);
|
|
86
|
+
else this.#taken.delete(region);
|
|
87
|
+
if (region.hidden !== hidden) region.hidden = hidden;
|
|
88
|
+
}
|
|
89
|
+
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
90
|
+
#give(region) {
|
|
91
|
+
if (this.#taken.delete(region)) region.hidden = false;
|
|
92
|
+
}
|
|
93
|
+
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
94
|
+
#inside(host, regions) {
|
|
95
|
+
return regions.filter((region) => host.contains(region));
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
58
99
|
// src/utils/string_list.ts
|
|
59
100
|
function parseStringList(raw, fallback = []) {
|
|
60
101
|
const text = raw.trim();
|
|
@@ -87,7 +128,12 @@ var IdleController = class extends Controller {
|
|
|
87
128
|
// runs, so one malformed attribute would stop the detector connecting.
|
|
88
129
|
events: { type: String, default: "" }
|
|
89
130
|
};
|
|
131
|
+
static targets = ["prompt", "idle"];
|
|
90
132
|
static events = ["prompt", "idle", "active"];
|
|
133
|
+
/** The regions of the warning window, revealed alongside `data-prompt`. */
|
|
134
|
+
#promptRegions = new StateRegions({ whenTrue: () => this.promptTargets });
|
|
135
|
+
/** The regions of the elapsed timeout, revealed alongside `data-idle`. */
|
|
136
|
+
#idleRegions = new StateRegions({ whenTrue: () => this.idleTargets });
|
|
91
137
|
#timeouts = new SafeTimeout();
|
|
92
138
|
#idle = false;
|
|
93
139
|
#prompted = false;
|
|
@@ -104,7 +150,7 @@ var IdleController = class extends Controller {
|
|
|
104
150
|
if (this.#idle || this.#prompted) {
|
|
105
151
|
this.#idle = false;
|
|
106
152
|
this.#prompted = false;
|
|
107
|
-
this
|
|
153
|
+
this.#reflect();
|
|
108
154
|
this.dispatch("active", { detail: {} });
|
|
109
155
|
this.#arm();
|
|
110
156
|
}
|
|
@@ -115,7 +161,7 @@ var IdleController = class extends Controller {
|
|
|
115
161
|
connect() {
|
|
116
162
|
this.#idle = false;
|
|
117
163
|
this.#prompted = false;
|
|
118
|
-
this
|
|
164
|
+
this.#reflect();
|
|
119
165
|
this.#boundEvents = parseStringList(this.eventsValue, DEFAULT_ACTIVITY_EVENTS);
|
|
120
166
|
for (const type of this.#boundEvents) {
|
|
121
167
|
document.addEventListener(type, this.#onActivity, { passive: true, capture: true });
|
|
@@ -131,7 +177,11 @@ var IdleController = class extends Controller {
|
|
|
131
177
|
document.removeEventListener("visibilitychange", this.#onVisibility);
|
|
132
178
|
this.#timeouts.clearAll();
|
|
133
179
|
}
|
|
134
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Schedules the prompt and idle checks from the current activity baseline.
|
|
182
|
+
*
|
|
183
|
+
* @stimeoRuntimeOnly `timeout` and `promptBefore` time the checks this call arms.
|
|
184
|
+
*/
|
|
135
185
|
#arm() {
|
|
136
186
|
this.#timeouts.clearAll();
|
|
137
187
|
this.#lastActivity = Date.now();
|
|
@@ -145,6 +195,9 @@ var IdleController = class extends Controller {
|
|
|
145
195
|
* Idle-timer callback: go idle only if there has genuinely been no activity for
|
|
146
196
|
* `timeout`; otherwise reschedule for the remaining time. This lets activity events
|
|
147
197
|
* stay O(1) (a timestamp write) while the deadline still tracks the last activity.
|
|
198
|
+
*
|
|
199
|
+
* @stimeoRuntimeOnly `timeout` sets the deadline this one check compares against; the phase it
|
|
200
|
+
* shows follows the elapsed time.
|
|
148
201
|
*/
|
|
149
202
|
#checkIdle() {
|
|
150
203
|
const remaining = this.timeoutValue - (Date.now() - this.#lastActivity);
|
|
@@ -154,19 +207,51 @@ var IdleController = class extends Controller {
|
|
|
154
207
|
}
|
|
155
208
|
this.#idle = true;
|
|
156
209
|
this.#prompted = false;
|
|
157
|
-
this
|
|
210
|
+
this.#reflect();
|
|
158
211
|
this.dispatch("idle", { detail: {} });
|
|
159
212
|
}
|
|
160
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* Prompt-timer callback: warn at `promptBefore` before the idle deadline. A window
|
|
215
|
+
* narrowed while the cycle runs can push the warning onto the deadline or past it,
|
|
216
|
+
* and the phase it belongs to is the one before idle, so a lapsed cycle keeps the
|
|
217
|
+
* phase it reached. The next window opens with the cycle that activity arms.
|
|
218
|
+
*
|
|
219
|
+
* @stimeoRuntimeOnly `timeout` and `promptBefore` set the deadlines this one check compares
|
|
220
|
+
* against; the phase it shows follows the elapsed time.
|
|
221
|
+
*/
|
|
161
222
|
#checkPrompt() {
|
|
223
|
+
if (this.#idle) return;
|
|
162
224
|
const remaining = this.timeoutValue - this.promptBeforeValue - (Date.now() - this.#lastActivity);
|
|
163
225
|
if (remaining > 0) {
|
|
164
226
|
this.#timeouts.set(() => this.#checkPrompt(), remaining);
|
|
165
227
|
return;
|
|
166
228
|
}
|
|
167
229
|
this.#prompted = true;
|
|
230
|
+
this.#reflect();
|
|
168
231
|
this.dispatch("prompt", { detail: { remaining: this.promptBeforeValue } });
|
|
169
232
|
}
|
|
233
|
+
/** Applies the current phase to a warning region inserted or replaced at runtime. */
|
|
234
|
+
promptTargetConnected() {
|
|
235
|
+
this.#promptRegions.reflect(this.element, this.#prompted);
|
|
236
|
+
}
|
|
237
|
+
/** Applies the current phase to an idle region inserted or replaced at runtime. */
|
|
238
|
+
idleTargetConnected() {
|
|
239
|
+
this.#idleRegions.reflect(this.element, this.#idle);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Writes the phase to the element and to the declared regions. Both hooks and both
|
|
243
|
+
* regions are a pure function of the two flags, so every place that moves a flag ends
|
|
244
|
+
* here and the markup can never disagree with the phase the detector is in.
|
|
245
|
+
*/
|
|
246
|
+
#reflect() {
|
|
247
|
+
const { element } = this;
|
|
248
|
+
if (this.#prompted) element.setAttribute("data-prompt", "true");
|
|
249
|
+
else element.removeAttribute("data-prompt");
|
|
250
|
+
if (this.#idle) element.setAttribute("data-idle", "true");
|
|
251
|
+
else element.removeAttribute("data-idle");
|
|
252
|
+
this.#promptRegions.reflect(element, this.#prompted);
|
|
253
|
+
this.#idleRegions.reflect(element, this.#idle);
|
|
254
|
+
}
|
|
170
255
|
};
|
|
171
256
|
|
|
172
257
|
export { IdleController };
|