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,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,113 +314,180 @@ function isScrollable(element) {
|
|
|
164
314
|
return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);
|
|
165
315
|
}
|
|
166
316
|
|
|
167
|
-
|
|
168
|
-
var
|
|
317
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
318
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
319
|
+
function stateReasonFor(event) {
|
|
320
|
+
if (!event) return "api";
|
|
321
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
322
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
323
|
+
return "user";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
var HoverCardController = class _HoverCardController extends Controller {
|
|
327
|
+
#numbers = new NumberValueReader();
|
|
169
328
|
static targets = ["trigger", "card"];
|
|
170
329
|
static values = {
|
|
171
330
|
openDelay: { type: Number, default: 300 },
|
|
172
331
|
closeDelay: { type: Number, default: 200 },
|
|
173
332
|
closeOnScroll: { type: Boolean, default: false }
|
|
174
333
|
};
|
|
334
|
+
static valueConstraints = {
|
|
335
|
+
openDelay: NUMBER_BOUNDS.timer,
|
|
336
|
+
closeDelay: NUMBER_BOUNDS.timer
|
|
337
|
+
};
|
|
175
338
|
static actions = ["close", "open"];
|
|
176
|
-
|
|
339
|
+
static events = ["close", "open"];
|
|
177
340
|
#timers = new SafeTimeout();
|
|
178
|
-
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
179
341
|
#escapeLayer = new EscapeLayer();
|
|
342
|
+
#expanded = new AttributeLease("aria-expanded", this.identifier);
|
|
180
343
|
#pendingOpen = null;
|
|
181
344
|
#pendingClose = null;
|
|
182
|
-
/** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */
|
|
183
345
|
#stopScrollDismiss = null;
|
|
184
|
-
|
|
346
|
+
#shownCard = null;
|
|
347
|
+
#connected = false;
|
|
348
|
+
#reporting = false;
|
|
185
349
|
connect() {
|
|
186
|
-
this.#
|
|
187
|
-
this.#
|
|
188
|
-
this.#
|
|
350
|
+
this.#cancelTimers();
|
|
351
|
+
this.#conceal("api");
|
|
352
|
+
this.#connected = true;
|
|
353
|
+
this.#reporting = true;
|
|
189
354
|
}
|
|
190
|
-
/** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */
|
|
191
355
|
disconnect() {
|
|
192
|
-
this.#
|
|
193
|
-
this.#
|
|
194
|
-
this.#
|
|
195
|
-
this.#
|
|
196
|
-
|
|
356
|
+
this.#connected = false;
|
|
357
|
+
this.#reporting = false;
|
|
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();
|
|
197
372
|
}
|
|
198
|
-
|
|
199
|
-
|
|
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();
|
|
379
|
+
}
|
|
380
|
+
open(event) {
|
|
381
|
+
const reason = stateReasonFor(event);
|
|
200
382
|
this.#cancelClose();
|
|
201
383
|
if (this.#isOpen || this.#pendingOpen !== null) return;
|
|
202
|
-
if (this
|
|
203
|
-
this.#reveal();
|
|
384
|
+
if (this.#safeOpenDelay <= 0) {
|
|
385
|
+
this.#reveal(reason);
|
|
204
386
|
return;
|
|
205
387
|
}
|
|
206
388
|
this.#pendingOpen = this.#timers.set(() => {
|
|
207
389
|
this.#pendingOpen = null;
|
|
208
|
-
this.#reveal();
|
|
209
|
-
}, this
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
* delayed callback re-checks whether focus has landed inside the controller
|
|
214
|
-
* (e.g. a link in the card) and, if so, aborts the close — covering keyboard
|
|
215
|
-
* traversal that the pointer-only hoverable bridge cannot.
|
|
216
|
-
*/
|
|
217
|
-
close() {
|
|
390
|
+
this.#reveal(reason);
|
|
391
|
+
}, this.#safeOpenDelay);
|
|
392
|
+
}
|
|
393
|
+
close(event) {
|
|
394
|
+
const reason = stateReasonFor(event);
|
|
218
395
|
this.#cancelOpen();
|
|
219
|
-
if (!this.#
|
|
396
|
+
if (!this.#closable || this.#pendingClose !== null) return;
|
|
220
397
|
this.#pendingClose = this.#timers.set(() => {
|
|
221
398
|
this.#pendingClose = null;
|
|
222
399
|
if (this.element.contains(document.activeElement)) return;
|
|
223
|
-
this.#conceal();
|
|
224
|
-
}, this
|
|
400
|
+
this.#conceal(reason);
|
|
401
|
+
}, this.#safeCloseDelay);
|
|
225
402
|
}
|
|
226
|
-
|
|
227
|
-
#reveal() {
|
|
403
|
+
#reveal(reason) {
|
|
228
404
|
if (!this.hasCardTarget) return;
|
|
229
|
-
this.cardTarget
|
|
230
|
-
this
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (this.
|
|
234
|
-
|
|
405
|
+
const card = this.cardTarget;
|
|
406
|
+
const was = this.#isOpen;
|
|
407
|
+
card.hidden = false;
|
|
408
|
+
card.setAttribute("data-state", "open");
|
|
409
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "true");
|
|
410
|
+
this.#shownCard = card;
|
|
411
|
+
if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
412
|
+
if (this.#shownCard !== card) return;
|
|
413
|
+
this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss("escape") });
|
|
414
|
+
this.#syncScrollDismiss();
|
|
415
|
+
}
|
|
416
|
+
#conceal(reason) {
|
|
417
|
+
const was = this.#isOpen;
|
|
418
|
+
this.#release();
|
|
419
|
+
this.#reflectClosed();
|
|
420
|
+
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
421
|
+
}
|
|
422
|
+
#reflectClosed() {
|
|
423
|
+
if (this.hasCardTarget) {
|
|
424
|
+
this.cardTarget.hidden = true;
|
|
425
|
+
this.cardTarget.setAttribute("data-state", "closed");
|
|
235
426
|
}
|
|
427
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "false");
|
|
236
428
|
}
|
|
237
|
-
|
|
238
|
-
|
|
429
|
+
#reflectTrigger() {
|
|
430
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, String(this.#isOpen));
|
|
431
|
+
}
|
|
432
|
+
#release() {
|
|
433
|
+
this.#shownCard = null;
|
|
239
434
|
this.#escapeLayer.deactivate();
|
|
240
435
|
this.#stopScrollDismiss?.();
|
|
241
436
|
this.#stopScrollDismiss = null;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
437
|
+
}
|
|
438
|
+
#syncScrollDismiss() {
|
|
439
|
+
if (this.closeOnScrollValue) {
|
|
440
|
+
this.#stopScrollDismiss ??= observeScrollDismiss(this.element, () => this.#dismiss("scroll"));
|
|
441
|
+
return;
|
|
245
442
|
}
|
|
246
|
-
|
|
443
|
+
this.#stopScrollDismiss?.();
|
|
444
|
+
this.#stopScrollDismiss = null;
|
|
247
445
|
}
|
|
248
|
-
|
|
249
|
-
|
|
446
|
+
#dismiss(reason) {
|
|
447
|
+
this.#cancelTimers();
|
|
448
|
+
this.#conceal(reason);
|
|
449
|
+
}
|
|
450
|
+
#cancelTimers() {
|
|
250
451
|
this.#cancelOpen();
|
|
251
452
|
this.#cancelClose();
|
|
252
|
-
this.#conceal();
|
|
253
453
|
}
|
|
254
|
-
/** Cancels any pending open timer. */
|
|
255
454
|
#cancelOpen() {
|
|
256
455
|
if (this.#pendingOpen !== null) {
|
|
257
456
|
this.#timers.clear(this.#pendingOpen);
|
|
258
457
|
this.#pendingOpen = null;
|
|
259
458
|
}
|
|
260
459
|
}
|
|
261
|
-
/** Cancels any pending close timer. */
|
|
262
460
|
#cancelClose() {
|
|
263
461
|
if (this.#pendingClose !== null) {
|
|
264
462
|
this.#timers.clear(this.#pendingClose);
|
|
265
463
|
this.#pendingClose = null;
|
|
266
464
|
}
|
|
267
465
|
}
|
|
268
|
-
/** Whether the card is currently visible. */
|
|
269
466
|
get #isOpen() {
|
|
270
467
|
return this.hasCardTarget && !this.cardTarget.hidden;
|
|
271
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
|
+
}
|
|
272
490
|
};
|
|
273
491
|
|
|
274
492
|
export { HoverCardController };
|
|
275
|
-
//# sourceMappingURL=hover_card_controller.js.map
|
|
276
493
|
//# sourceMappingURL=hover_card_controller.js.map
|