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,20 +1,158 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function sharedRegistry(key, create) {
|
|
4
|
+
const symbol = Symbol.for(key);
|
|
5
|
+
const scope = globalThis;
|
|
6
|
+
const existing = scope[symbol];
|
|
7
|
+
if (existing !== void 0) return existing;
|
|
8
|
+
const registry = create();
|
|
9
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
10
|
+
return registry;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isAttributeValue(value) {
|
|
14
|
+
return value === null || typeof value === "string";
|
|
15
|
+
}
|
|
16
|
+
function readLeaseRecord(element, record) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
19
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
20
|
+
const [original, written, ...rest] = parsed;
|
|
21
|
+
const linked = rest[0] !== false;
|
|
22
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
23
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
24
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
25
|
+
} catch {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var AttributeLease = class _AttributeLease {
|
|
30
|
+
static #registry = sharedRegistry(
|
|
31
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
32
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
33
|
+
);
|
|
34
|
+
#attribute;
|
|
35
|
+
#record;
|
|
36
|
+
#base;
|
|
37
|
+
#held = /* @__PURE__ */ new Set();
|
|
38
|
+
constructor(attribute, owner, options = {}) {
|
|
39
|
+
this.#attribute = attribute;
|
|
40
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
41
|
+
this.#base = options.base === true;
|
|
42
|
+
const names = _AttributeLease.#registry.names;
|
|
43
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
44
|
+
}
|
|
45
|
+
write(element, value) {
|
|
46
|
+
const stack = this.#stack(element);
|
|
47
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
48
|
+
this.#held.add(element);
|
|
49
|
+
const shows = !this.#covered(element, stack, hold);
|
|
50
|
+
hold.written = value;
|
|
51
|
+
this.#mark(element, stack);
|
|
52
|
+
if (shows) this.#reflect(element, value);
|
|
53
|
+
}
|
|
54
|
+
return(element) {
|
|
55
|
+
const held = this.#held.delete(element);
|
|
56
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
57
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
58
|
+
const hold = this.#take(stack);
|
|
59
|
+
if (!hold) return false;
|
|
60
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
61
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
62
|
+
const index = stack.indexOf(hold);
|
|
63
|
+
stack.splice(index, 1);
|
|
64
|
+
const above = stack[index];
|
|
65
|
+
if (above) above.linked &&= hold.linked;
|
|
66
|
+
this.#mark(element, stack);
|
|
67
|
+
if (!owns) return true;
|
|
68
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
69
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
returnAll() {
|
|
73
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
74
|
+
}
|
|
75
|
+
#stack(element) {
|
|
76
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
77
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
78
|
+
stacks.set(element, byAttribute);
|
|
79
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
80
|
+
byAttribute.set(this.#attribute, stack);
|
|
81
|
+
return stack;
|
|
82
|
+
}
|
|
83
|
+
#seed(element) {
|
|
84
|
+
const found = [];
|
|
85
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
86
|
+
const read = readLeaseRecord(element, record);
|
|
87
|
+
if (read) found.push({ ...read, record });
|
|
88
|
+
}
|
|
89
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
90
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
91
|
+
lease: null,
|
|
92
|
+
record,
|
|
93
|
+
written,
|
|
94
|
+
original,
|
|
95
|
+
linked: linked && index > 0
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
#take(stack) {
|
|
99
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
100
|
+
if (hold) hold.lease = this;
|
|
101
|
+
return hold;
|
|
102
|
+
}
|
|
103
|
+
#acquire(element, stack) {
|
|
104
|
+
const current = element.getAttribute(this.#attribute);
|
|
105
|
+
const top = stack.at(-1);
|
|
106
|
+
const bottom = stack[0];
|
|
107
|
+
const over = top?.written === current ? top : void 0;
|
|
108
|
+
const from = this.#base ? bottom : over;
|
|
109
|
+
const hold = {
|
|
110
|
+
lease: this,
|
|
111
|
+
record: this.#record,
|
|
112
|
+
written: current,
|
|
113
|
+
original: from ? from.original : current,
|
|
114
|
+
linked: !this.#base && over !== void 0
|
|
115
|
+
};
|
|
116
|
+
if (this.#base && bottom) {
|
|
117
|
+
bottom.linked = true;
|
|
118
|
+
stack.unshift(hold);
|
|
119
|
+
} else stack.push(hold);
|
|
120
|
+
return hold;
|
|
121
|
+
}
|
|
122
|
+
#covered(element, stack, hold) {
|
|
123
|
+
const current = element.getAttribute(this.#attribute);
|
|
124
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
125
|
+
}
|
|
126
|
+
#mark(element, stack) {
|
|
127
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
128
|
+
stack.forEach((hold, index) => {
|
|
129
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
130
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
131
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
132
|
+
records.set(
|
|
133
|
+
hold.record,
|
|
134
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
for (const [name, recorded] of records) {
|
|
138
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
139
|
+
if (recorded === null) element.removeAttribute(name);
|
|
140
|
+
else element.setAttribute(name, recorded);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
#reflect(element, value) {
|
|
144
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
145
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
146
|
+
else element.setAttribute(this.#attribute, value);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
4
149
|
|
|
5
|
-
// src/utils/escape_layer.ts
|
|
6
150
|
var EscapeLayer = class _EscapeLayer {
|
|
7
|
-
static #registries =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
#claims = null;
|
|
13
|
-
/**
|
|
14
|
-
* Activates this layer at the top of its document's Escape stack, installing
|
|
15
|
-
* the document's shared resolver listener if this is its first layer.
|
|
16
|
-
* Re-activating an already-active layer moves it to the top.
|
|
17
|
-
*/
|
|
151
|
+
static #registries = sharedRegistry(
|
|
152
|
+
"stimeo-ui.escape-layer.registry.v1",
|
|
153
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
154
|
+
);
|
|
155
|
+
#active = null;
|
|
18
156
|
activate(ownerDocument = document, options) {
|
|
19
157
|
this.deactivate();
|
|
20
158
|
let registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
@@ -23,44 +161,31 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
23
161
|
_EscapeLayer.#registries.set(ownerDocument, registry);
|
|
24
162
|
ownerDocument.addEventListener("keydown", registry.onKeydown);
|
|
25
163
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
this.#
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Removes this layer from its document's Escape stack, uninstalling the
|
|
33
|
-
* shared listener when the stack empties. Safe to call when inactive.
|
|
34
|
-
*/
|
|
164
|
+
const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
|
|
165
|
+
registry.stack.push(entry);
|
|
166
|
+
this.#active = { document: ownerDocument, entry };
|
|
167
|
+
}
|
|
35
168
|
deactivate() {
|
|
36
|
-
const
|
|
37
|
-
if (!
|
|
38
|
-
const registry = _EscapeLayer.#registries.get(
|
|
169
|
+
const active = this.#active;
|
|
170
|
+
if (!active) return;
|
|
171
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
39
172
|
if (registry) {
|
|
40
|
-
const index = registry.stack.lastIndexOf(
|
|
173
|
+
const index = registry.stack.lastIndexOf(active.entry);
|
|
41
174
|
if (index >= 0) registry.stack.splice(index, 1);
|
|
42
175
|
if (registry.stack.length === 0) {
|
|
43
|
-
|
|
44
|
-
_EscapeLayer.#registries.delete(
|
|
176
|
+
active.document.removeEventListener("keydown", registry.onKeydown);
|
|
177
|
+
_EscapeLayer.#registries.delete(active.document);
|
|
45
178
|
}
|
|
46
179
|
}
|
|
47
|
-
this.#
|
|
48
|
-
|
|
49
|
-
this.#claims = null;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Whether this active layer would own a press right now: it is the topmost
|
|
53
|
-
* layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
|
|
54
|
-
* and diagnostics — production dismissal goes through the shared listener.
|
|
55
|
-
*/
|
|
180
|
+
this.#active = null;
|
|
181
|
+
}
|
|
56
182
|
get ownsEscape() {
|
|
57
|
-
const
|
|
58
|
-
if (!
|
|
59
|
-
const registry = _EscapeLayer.#registries.get(
|
|
183
|
+
const active = this.#active;
|
|
184
|
+
if (!active) return false;
|
|
185
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
60
186
|
if (!registry) return false;
|
|
61
|
-
return _EscapeLayer.#resolveOwner(registry.stack) ===
|
|
187
|
+
return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
|
|
62
188
|
}
|
|
63
|
-
/** Builds a document's registry with its shared resolver listener. */
|
|
64
189
|
static #createRegistry() {
|
|
65
190
|
const registry = {
|
|
66
191
|
stack: [],
|
|
@@ -69,60 +194,86 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
69
194
|
const owner = _EscapeLayer.#resolveOwner(registry.stack);
|
|
70
195
|
if (!owner) return;
|
|
71
196
|
event.preventDefault();
|
|
72
|
-
owner
|
|
197
|
+
owner.onDismiss();
|
|
73
198
|
}
|
|
74
199
|
};
|
|
75
200
|
return registry;
|
|
76
201
|
}
|
|
77
|
-
/** The topmost stack layer whose claims predicate passes, or `null`. */
|
|
78
202
|
static #resolveOwner(stack) {
|
|
79
203
|
for (let index = stack.length - 1; index >= 0; index--) {
|
|
80
204
|
const layer = stack[index];
|
|
81
205
|
if (!layer) continue;
|
|
82
|
-
if (layer
|
|
206
|
+
if (layer.claims && !layer.claims()) continue;
|
|
83
207
|
return layer;
|
|
84
208
|
}
|
|
85
209
|
return null;
|
|
86
210
|
}
|
|
87
211
|
};
|
|
88
212
|
|
|
89
|
-
|
|
213
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
214
|
+
|
|
215
|
+
var NUMBER_BOUNDS = {
|
|
216
|
+
timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
|
|
217
|
+
function matchesNumberBounds(value, bounds) {
|
|
218
|
+
if (!Number.isFinite(value)) {
|
|
219
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
220
|
+
if (direction === null) return false;
|
|
221
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
222
|
+
}
|
|
223
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
224
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
225
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
226
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
227
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function readNumber(raw, fallback, bounds) {
|
|
232
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
var NumberValueReader = class {
|
|
236
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
237
|
+
read(owner, name, raw, fallback, bounds) {
|
|
238
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
239
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
240
|
+
this.#lastRejected.delete(name);
|
|
241
|
+
return resolved;
|
|
242
|
+
}
|
|
243
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
244
|
+
const literal = owner.element.getAttribute(attribute);
|
|
245
|
+
if (literal === null) {
|
|
246
|
+
this.#lastRejected.delete(name);
|
|
247
|
+
return resolved;
|
|
248
|
+
}
|
|
249
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
250
|
+
this.#lastRejected.set(name, literal);
|
|
251
|
+
console.warn(
|
|
252
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return resolved;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
90
259
|
var TimerRegistry = class {
|
|
91
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
92
260
|
ids = /* @__PURE__ */ new Set();
|
|
93
|
-
/**
|
|
94
|
-
* Cancels a single tracked timer.
|
|
95
|
-
*
|
|
96
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
97
|
-
* registry), so callers can clear defensively without guarding.
|
|
98
|
-
*/
|
|
99
261
|
clear(id) {
|
|
100
262
|
if (this.ids.delete(id)) {
|
|
101
263
|
this.cancel(id);
|
|
102
264
|
}
|
|
103
265
|
}
|
|
104
|
-
/**
|
|
105
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
106
|
-
* to guarantee no timer outlives the element.
|
|
107
|
-
*/
|
|
108
266
|
clearAll() {
|
|
109
267
|
for (const id of this.ids) {
|
|
110
268
|
this.cancel(id);
|
|
111
269
|
}
|
|
112
270
|
this.ids.clear();
|
|
113
271
|
}
|
|
114
|
-
/** Number of timers currently tracked (pending). */
|
|
115
272
|
get size() {
|
|
116
273
|
return this.ids.size;
|
|
117
274
|
}
|
|
118
275
|
};
|
|
119
276
|
var SafeTimeout = class extends TimerRegistry {
|
|
120
|
-
/**
|
|
121
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
122
|
-
*
|
|
123
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
124
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
125
|
-
*/
|
|
126
277
|
set(callback, delay) {
|
|
127
278
|
const id = this.schedule(() => {
|
|
128
279
|
this.ids.delete(id);
|
|
@@ -139,7 +290,6 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
139
290
|
}
|
|
140
291
|
};
|
|
141
292
|
|
|
142
|
-
// src/utils/scroll_dismiss.ts
|
|
143
293
|
function observeScrollDismiss(element, onScroll) {
|
|
144
294
|
const targets = [...scrollParents(element), window];
|
|
145
295
|
const handler = () => onScroll();
|
|
@@ -164,7 +314,6 @@ function isScrollable(element) {
|
|
|
164
314
|
return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);
|
|
165
315
|
}
|
|
166
316
|
|
|
167
|
-
// src/utils/state_reason.ts
|
|
168
317
|
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
169
318
|
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
170
319
|
function stateReasonFor(event) {
|
|
@@ -174,129 +323,171 @@ function stateReasonFor(event) {
|
|
|
174
323
|
return "user";
|
|
175
324
|
}
|
|
176
325
|
|
|
177
|
-
|
|
178
|
-
|
|
326
|
+
var HoverCardController = class _HoverCardController extends Controller {
|
|
327
|
+
#numbers = new NumberValueReader();
|
|
179
328
|
static targets = ["trigger", "card"];
|
|
180
329
|
static values = {
|
|
181
330
|
openDelay: { type: Number, default: 300 },
|
|
182
331
|
closeDelay: { type: Number, default: 200 },
|
|
183
332
|
closeOnScroll: { type: Boolean, default: false }
|
|
184
333
|
};
|
|
334
|
+
static valueConstraints = {
|
|
335
|
+
openDelay: NUMBER_BOUNDS.timer,
|
|
336
|
+
closeDelay: NUMBER_BOUNDS.timer
|
|
337
|
+
};
|
|
185
338
|
static actions = ["close", "open"];
|
|
186
339
|
static events = ["close", "open"];
|
|
187
|
-
/** Pending open/close timers, with their IDs reset on every lifecycle boundary. */
|
|
188
340
|
#timers = new SafeTimeout();
|
|
189
|
-
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
190
341
|
#escapeLayer = new EscapeLayer();
|
|
342
|
+
#expanded = new AttributeLease("aria-expanded", this.identifier);
|
|
191
343
|
#pendingOpen = null;
|
|
192
344
|
#pendingClose = null;
|
|
193
|
-
/** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */
|
|
194
345
|
#stopScrollDismiss = null;
|
|
195
|
-
|
|
346
|
+
#shownCard = null;
|
|
347
|
+
#connected = false;
|
|
196
348
|
#reporting = false;
|
|
197
|
-
/** Starts closed and discards any stale pending state from a prior connection. */
|
|
198
349
|
connect() {
|
|
199
|
-
this.#
|
|
200
|
-
this.#cancelClose();
|
|
350
|
+
this.#cancelTimers();
|
|
201
351
|
this.#conceal("api");
|
|
352
|
+
this.#connected = true;
|
|
202
353
|
this.#reporting = true;
|
|
203
354
|
}
|
|
204
|
-
/** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */
|
|
205
355
|
disconnect() {
|
|
356
|
+
this.#connected = false;
|
|
206
357
|
this.#reporting = false;
|
|
207
|
-
this.#
|
|
208
|
-
this.#
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
this.#
|
|
358
|
+
this.#cancelTimers();
|
|
359
|
+
this.#release();
|
|
360
|
+
}
|
|
361
|
+
closeOnScrollValueChanged() {
|
|
362
|
+
if (this.#shownCard) this.#syncScrollDismiss();
|
|
363
|
+
}
|
|
364
|
+
cardTargetDisconnected(card) {
|
|
365
|
+
if (card !== this.#shownCard) return;
|
|
366
|
+
if (this.cardTargets.includes(card)) return;
|
|
367
|
+
this.#cancelTimers();
|
|
368
|
+
this.#release();
|
|
369
|
+
card.hidden = true;
|
|
370
|
+
card.setAttribute("data-state", "closed");
|
|
371
|
+
this.#reflectClosed();
|
|
372
|
+
}
|
|
373
|
+
triggerTargetConnected() {
|
|
374
|
+
if (this.#connected) this.#reflectTrigger();
|
|
375
|
+
}
|
|
376
|
+
triggerTargetDisconnected(trigger) {
|
|
377
|
+
if (!this.triggerTargets.includes(trigger)) this.#expanded.return(trigger);
|
|
378
|
+
if (this.#connected) this.#reflectTrigger();
|
|
212
379
|
}
|
|
213
|
-
/** Opens the card, after `openDelay` ms (or immediately at 0). Cancels a pending close. */
|
|
214
380
|
open(event) {
|
|
215
381
|
const reason = stateReasonFor(event);
|
|
216
382
|
this.#cancelClose();
|
|
217
383
|
if (this.#isOpen || this.#pendingOpen !== null) return;
|
|
218
|
-
if (this
|
|
384
|
+
if (this.#safeOpenDelay <= 0) {
|
|
219
385
|
this.#reveal(reason);
|
|
220
386
|
return;
|
|
221
387
|
}
|
|
222
388
|
this.#pendingOpen = this.#timers.set(() => {
|
|
223
389
|
this.#pendingOpen = null;
|
|
224
390
|
this.#reveal(reason);
|
|
225
|
-
}, this
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Schedules the card to close after `closeDelay`. Cancels a pending open. The
|
|
229
|
-
* delayed callback re-checks whether focus has landed inside the controller
|
|
230
|
-
* (e.g. a link in the card) and, if so, aborts the close — covering keyboard
|
|
231
|
-
* traversal that the pointer-only hoverable bridge cannot.
|
|
232
|
-
*/
|
|
391
|
+
}, this.#safeOpenDelay);
|
|
392
|
+
}
|
|
233
393
|
close(event) {
|
|
234
394
|
const reason = stateReasonFor(event);
|
|
235
395
|
this.#cancelOpen();
|
|
236
|
-
if (!this.#
|
|
396
|
+
if (!this.#closable || this.#pendingClose !== null) return;
|
|
237
397
|
this.#pendingClose = this.#timers.set(() => {
|
|
238
398
|
this.#pendingClose = null;
|
|
239
399
|
if (this.element.contains(document.activeElement)) return;
|
|
240
400
|
this.#conceal(reason);
|
|
241
|
-
}, this
|
|
242
|
-
}
|
|
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
|
-
*/
|
|
401
|
+
}, this.#safeCloseDelay);
|
|
402
|
+
}
|
|
249
403
|
#reveal(reason) {
|
|
250
404
|
if (!this.hasCardTarget) return;
|
|
405
|
+
const card = this.cardTarget;
|
|
251
406
|
const was = this.#isOpen;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (this.hasTriggerTarget) this.triggerTarget
|
|
407
|
+
card.hidden = false;
|
|
408
|
+
card.setAttribute("data-state", "open");
|
|
409
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "true");
|
|
410
|
+
this.#shownCard = card;
|
|
255
411
|
if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
412
|
+
if (this.#shownCard !== card) return;
|
|
256
413
|
this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss("escape") });
|
|
257
|
-
|
|
258
|
-
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss("scroll"));
|
|
259
|
-
}
|
|
414
|
+
this.#syncScrollDismiss();
|
|
260
415
|
}
|
|
261
|
-
/** Hides the card, reflects state, reports a move, and leaves the Escape stack. */
|
|
262
416
|
#conceal(reason) {
|
|
263
417
|
const was = this.#isOpen;
|
|
264
|
-
this.#
|
|
265
|
-
this.#
|
|
266
|
-
this.#
|
|
418
|
+
this.#release();
|
|
419
|
+
this.#reflectClosed();
|
|
420
|
+
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
421
|
+
}
|
|
422
|
+
#reflectClosed() {
|
|
267
423
|
if (this.hasCardTarget) {
|
|
268
424
|
this.cardTarget.hidden = true;
|
|
269
425
|
this.cardTarget.setAttribute("data-state", "closed");
|
|
270
426
|
}
|
|
271
|
-
if (this.hasTriggerTarget) this.triggerTarget
|
|
272
|
-
|
|
427
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "false");
|
|
428
|
+
}
|
|
429
|
+
#reflectTrigger() {
|
|
430
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, String(this.#isOpen));
|
|
431
|
+
}
|
|
432
|
+
#release() {
|
|
433
|
+
this.#shownCard = null;
|
|
434
|
+
this.#escapeLayer.deactivate();
|
|
435
|
+
this.#stopScrollDismiss?.();
|
|
436
|
+
this.#stopScrollDismiss = null;
|
|
437
|
+
}
|
|
438
|
+
#syncScrollDismiss() {
|
|
439
|
+
if (this.closeOnScrollValue) {
|
|
440
|
+
this.#stopScrollDismiss ??= observeScrollDismiss(this.element, () => this.#dismiss("scroll"));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
this.#stopScrollDismiss?.();
|
|
444
|
+
this.#stopScrollDismiss = null;
|
|
273
445
|
}
|
|
274
|
-
/** Cancels pending timers and conceals immediately (shared Escape path). */
|
|
275
446
|
#dismiss(reason) {
|
|
447
|
+
this.#cancelTimers();
|
|
448
|
+
this.#conceal(reason);
|
|
449
|
+
}
|
|
450
|
+
#cancelTimers() {
|
|
276
451
|
this.#cancelOpen();
|
|
277
452
|
this.#cancelClose();
|
|
278
|
-
this.#conceal(reason);
|
|
279
453
|
}
|
|
280
|
-
/** Cancels any pending open timer. */
|
|
281
454
|
#cancelOpen() {
|
|
282
455
|
if (this.#pendingOpen !== null) {
|
|
283
456
|
this.#timers.clear(this.#pendingOpen);
|
|
284
457
|
this.#pendingOpen = null;
|
|
285
458
|
}
|
|
286
459
|
}
|
|
287
|
-
/** Cancels any pending close timer. */
|
|
288
460
|
#cancelClose() {
|
|
289
461
|
if (this.#pendingClose !== null) {
|
|
290
462
|
this.#timers.clear(this.#pendingClose);
|
|
291
463
|
this.#pendingClose = null;
|
|
292
464
|
}
|
|
293
465
|
}
|
|
294
|
-
/** Whether the card is currently visible. */
|
|
295
466
|
get #isOpen() {
|
|
296
467
|
return this.hasCardTarget && !this.cardTarget.hidden;
|
|
297
468
|
}
|
|
469
|
+
get #closable() {
|
|
470
|
+
return this.#isOpen || this.#shownCard !== null;
|
|
471
|
+
}
|
|
472
|
+
get #safeOpenDelay() {
|
|
473
|
+
return this.#numbers.read(
|
|
474
|
+
this,
|
|
475
|
+
"openDelay",
|
|
476
|
+
this.openDelayValue,
|
|
477
|
+
_HoverCardController.values.openDelay.default,
|
|
478
|
+
_HoverCardController.valueConstraints.openDelay
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
get #safeCloseDelay() {
|
|
482
|
+
return this.#numbers.read(
|
|
483
|
+
this,
|
|
484
|
+
"closeDelay",
|
|
485
|
+
this.closeDelayValue,
|
|
486
|
+
_HoverCardController.values.closeDelay.default,
|
|
487
|
+
_HoverCardController.valueConstraints.closeDelay
|
|
488
|
+
);
|
|
489
|
+
}
|
|
298
490
|
};
|
|
299
491
|
|
|
300
492
|
export { HoverCardController };
|
|
301
|
-
//# sourceMappingURL=hover_card_controller.js.map
|
|
302
493
|
//# sourceMappingURL=hover_card_controller.js.map
|