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,89 +1,183 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
4
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
5
|
+
function stateReasonFor(event) {
|
|
6
|
+
if (!event) return "api";
|
|
7
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
8
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
9
|
+
return "user";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function actionSource(source) {
|
|
13
|
+
if (source instanceof HTMLElement) {
|
|
14
|
+
return { event: null, host: source, origin: source, reason: "api" };
|
|
15
|
+
}
|
|
16
|
+
const event = source ?? null;
|
|
17
|
+
return {
|
|
18
|
+
event,
|
|
19
|
+
host: event?.currentTarget instanceof Element ? event.currentTarget : null,
|
|
20
|
+
origin: event?.target instanceof Element ? event.target : null,
|
|
21
|
+
reason: stateReasonFor(event)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
4
24
|
|
|
5
|
-
// src/utils/logical_scroll.ts
|
|
6
25
|
function isRtl(element) {
|
|
7
26
|
return window.getComputedStyle(element).direction === "rtl";
|
|
8
27
|
}
|
|
9
28
|
|
|
10
|
-
// src/utils/arrow_step.ts
|
|
11
29
|
function isReservedArrowChord(event, allow = []) {
|
|
12
30
|
if (!event.key.startsWith("Arrow")) return false;
|
|
13
31
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
14
32
|
}
|
|
15
33
|
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
function sharedRegistry(key, create) {
|
|
35
|
+
const symbol = Symbol.for(key);
|
|
36
|
+
const scope = globalThis;
|
|
37
|
+
const existing = scope[symbol];
|
|
38
|
+
if (existing !== void 0) return existing;
|
|
39
|
+
const registry = create();
|
|
40
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
41
|
+
return registry;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isAttributeValue(value) {
|
|
45
|
+
return value === null || typeof value === "string";
|
|
46
|
+
}
|
|
47
|
+
function readLeaseRecord(element, record) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
50
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
51
|
+
const [original, written, ...rest] = parsed;
|
|
52
|
+
const linked = rest[0] !== false;
|
|
53
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
54
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
55
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
56
|
+
} catch {
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
var AttributeLease = class _AttributeLease {
|
|
61
|
+
static #registry = sharedRegistry(
|
|
62
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
63
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
64
|
+
);
|
|
18
65
|
#attribute;
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
66
|
+
#record;
|
|
67
|
+
#base;
|
|
68
|
+
#held = /* @__PURE__ */ new Set();
|
|
69
|
+
constructor(attribute, owner, options = {}) {
|
|
22
70
|
this.#attribute = attribute;
|
|
71
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
72
|
+
this.#base = options.base === true;
|
|
73
|
+
const names = _AttributeLease.#registry.names;
|
|
74
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
23
75
|
}
|
|
24
|
-
/** Writes or removes the leased attribute while preserving its authored value. */
|
|
25
76
|
write(element, value) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
this.#reflect(element, value);
|
|
77
|
+
const stack = this.#stack(element);
|
|
78
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
79
|
+
this.#held.add(element);
|
|
80
|
+
const shows = !this.#covered(element, stack, hold);
|
|
81
|
+
hold.written = value;
|
|
82
|
+
this.#mark(element, stack);
|
|
83
|
+
if (shows) this.#reflect(element, value);
|
|
36
84
|
}
|
|
37
|
-
/** Returns one lease without overwriting a value subsequently authored by a consumer. */
|
|
38
85
|
return(element) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
this.#
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
86
|
+
const held = this.#held.delete(element);
|
|
87
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
88
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
89
|
+
const hold = this.#take(stack);
|
|
90
|
+
if (!hold) return false;
|
|
91
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
92
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
93
|
+
const index = stack.indexOf(hold);
|
|
94
|
+
stack.splice(index, 1);
|
|
95
|
+
const above = stack[index];
|
|
96
|
+
if (above) above.linked &&= hold.linked;
|
|
97
|
+
this.#mark(element, stack);
|
|
98
|
+
if (!owns) return true;
|
|
99
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
100
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
returnAll() {
|
|
104
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
105
|
+
}
|
|
106
|
+
#stack(element) {
|
|
107
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
108
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
109
|
+
stacks.set(element, byAttribute);
|
|
110
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
111
|
+
byAttribute.set(this.#attribute, stack);
|
|
112
|
+
return stack;
|
|
113
|
+
}
|
|
114
|
+
#seed(element) {
|
|
115
|
+
const found = [];
|
|
116
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
117
|
+
const read = readLeaseRecord(element, record);
|
|
118
|
+
if (read) found.push({ ...read, record });
|
|
119
|
+
}
|
|
120
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
121
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
122
|
+
lease: null,
|
|
123
|
+
record,
|
|
124
|
+
written,
|
|
125
|
+
original,
|
|
126
|
+
linked: linked && index > 0
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
#take(stack) {
|
|
130
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
131
|
+
if (hold) hold.lease = this;
|
|
132
|
+
return hold;
|
|
133
|
+
}
|
|
134
|
+
#acquire(element, stack) {
|
|
135
|
+
const current = element.getAttribute(this.#attribute);
|
|
136
|
+
const top = stack.at(-1);
|
|
137
|
+
const bottom = stack[0];
|
|
138
|
+
const over = top?.written === current ? top : void 0;
|
|
139
|
+
const from = this.#base ? bottom : over;
|
|
140
|
+
const hold = {
|
|
141
|
+
lease: this,
|
|
142
|
+
record: this.#record,
|
|
143
|
+
written: current,
|
|
144
|
+
original: from ? from.original : current,
|
|
145
|
+
linked: !this.#base && over !== void 0
|
|
146
|
+
};
|
|
147
|
+
if (this.#base && bottom) {
|
|
148
|
+
bottom.linked = true;
|
|
149
|
+
stack.unshift(hold);
|
|
150
|
+
} else stack.push(hold);
|
|
151
|
+
return hold;
|
|
152
|
+
}
|
|
153
|
+
#covered(element, stack, hold) {
|
|
154
|
+
const current = element.getAttribute(this.#attribute);
|
|
155
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
156
|
+
}
|
|
157
|
+
#mark(element, stack) {
|
|
158
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
159
|
+
stack.forEach((hold, index) => {
|
|
160
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
161
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
162
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
163
|
+
records.set(
|
|
164
|
+
hold.record,
|
|
165
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
for (const [name, recorded] of records) {
|
|
169
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
170
|
+
if (recorded === null) element.removeAttribute(name);
|
|
171
|
+
else element.setAttribute(name, recorded);
|
|
172
|
+
}
|
|
44
173
|
}
|
|
45
|
-
/** Reflects only a real value transition, avoiding self-triggered mutation work. */
|
|
46
174
|
#reflect(element, value) {
|
|
47
175
|
if (element.getAttribute(this.#attribute) === value) return;
|
|
48
176
|
if (value === null) element.removeAttribute(this.#attribute);
|
|
49
177
|
else element.setAttribute(this.#attribute, value);
|
|
50
178
|
}
|
|
51
|
-
/** Returns every outstanding lease using the same ownership check as {@link return}. */
|
|
52
|
-
returnAll() {
|
|
53
|
-
for (const element of Array.from(this.#records.keys())) this.return(element);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// src/utils/before_cache_reset.ts
|
|
58
|
-
var BeforeCacheReset = class _BeforeCacheReset {
|
|
59
|
-
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
60
|
-
static #subscribers = /* @__PURE__ */ new Set();
|
|
61
|
-
/** The shared listener; installed while at least one instance is subscribed. */
|
|
62
|
-
static #onBeforeCache = () => {
|
|
63
|
-
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
64
|
-
};
|
|
65
|
-
#rewind;
|
|
66
|
-
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
67
|
-
constructor(rewind) {
|
|
68
|
-
this.#rewind = rewind;
|
|
69
|
-
}
|
|
70
|
-
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
71
|
-
activate() {
|
|
72
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
73
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
74
|
-
if (first) {
|
|
75
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
79
|
-
deactivate() {
|
|
80
|
-
_BeforeCacheReset.#subscribers.delete(this);
|
|
81
|
-
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
82
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
83
|
-
}
|
|
84
179
|
};
|
|
85
180
|
|
|
86
|
-
// src/utils/field_mirror.ts
|
|
87
181
|
function writeField(field, value) {
|
|
88
182
|
if (field.value === value) return false;
|
|
89
183
|
field.value = value;
|
|
@@ -93,27 +187,33 @@ function commitField(target) {
|
|
|
93
187
|
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
94
188
|
}
|
|
95
189
|
|
|
96
|
-
|
|
190
|
+
var ListenerSet = class {
|
|
191
|
+
#abort = new AbortController();
|
|
192
|
+
add(target, type, handler, options) {
|
|
193
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
194
|
+
}
|
|
195
|
+
dispose() {
|
|
196
|
+
this.#abort.abort();
|
|
197
|
+
this.#abort = new AbortController();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
97
201
|
var MicrotaskCoalescer = class {
|
|
98
202
|
#run;
|
|
99
203
|
#queued = false;
|
|
100
204
|
#active = false;
|
|
101
205
|
#generation = 0;
|
|
102
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
103
206
|
constructor(run) {
|
|
104
207
|
this.#run = run;
|
|
105
208
|
}
|
|
106
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
107
209
|
activate() {
|
|
108
210
|
this.#active = true;
|
|
109
211
|
}
|
|
110
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
111
212
|
cancel() {
|
|
112
213
|
this.#active = false;
|
|
113
214
|
this.#queued = false;
|
|
114
215
|
this.#generation += 1;
|
|
115
216
|
}
|
|
116
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
117
217
|
schedule() {
|
|
118
218
|
if (!this.#active || this.#queued) return;
|
|
119
219
|
this.#queued = true;
|
|
@@ -126,31 +226,94 @@ var MicrotaskCoalescer = class {
|
|
|
126
226
|
}
|
|
127
227
|
};
|
|
128
228
|
|
|
129
|
-
|
|
229
|
+
var MorphRenderWatcher = class {
|
|
230
|
+
#listeners = new ListenerSet();
|
|
231
|
+
#pass;
|
|
232
|
+
#element = null;
|
|
233
|
+
#onMorph = () => this.#pass.schedule();
|
|
234
|
+
constructor(run) {
|
|
235
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
236
|
+
}
|
|
237
|
+
observe(element) {
|
|
238
|
+
if (this.#element === element) return;
|
|
239
|
+
this.disconnect();
|
|
240
|
+
this.#element = element;
|
|
241
|
+
this.#pass.activate();
|
|
242
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
243
|
+
}
|
|
244
|
+
schedule() {
|
|
245
|
+
this.#pass.schedule();
|
|
246
|
+
}
|
|
247
|
+
disconnect() {
|
|
248
|
+
this.#listeners.dispose();
|
|
249
|
+
this.#pass.cancel();
|
|
250
|
+
this.#element = null;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
var MoveCounter = class {
|
|
255
|
+
#sequence = 0;
|
|
256
|
+
record() {
|
|
257
|
+
this.#sequence += 1;
|
|
258
|
+
return this.#sequence;
|
|
259
|
+
}
|
|
260
|
+
isLatest(token) {
|
|
261
|
+
return token === this.#sequence;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
var NUMBER_BOUNDS = {
|
|
266
|
+
finite: { finite: true }};
|
|
267
|
+
function matchesNumberBounds(value, bounds) {
|
|
268
|
+
if (!Number.isFinite(value)) {
|
|
269
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
270
|
+
if (direction === null) return false;
|
|
271
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
272
|
+
}
|
|
273
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
274
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
275
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
276
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
277
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function readNumber(raw, fallback, bounds) {
|
|
282
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
var NumberValueReader = class {
|
|
286
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
287
|
+
read(owner, name, raw, fallback, bounds) {
|
|
288
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
289
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
290
|
+
this.#lastRejected.delete(name);
|
|
291
|
+
return resolved;
|
|
292
|
+
}
|
|
293
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
294
|
+
const literal = owner.element.getAttribute(attribute);
|
|
295
|
+
if (literal === null) {
|
|
296
|
+
this.#lastRejected.delete(name);
|
|
297
|
+
return resolved;
|
|
298
|
+
}
|
|
299
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
300
|
+
this.#lastRejected.set(name, literal);
|
|
301
|
+
console.warn(
|
|
302
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return resolved;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
130
309
|
var RovingTabindex = class {
|
|
131
|
-
/** Returns the current ordered item elements; called on every operation. */
|
|
132
310
|
#getItems;
|
|
133
|
-
/**
|
|
134
|
-
* @param getItems - Returns the current ordered item elements. Called on every
|
|
135
|
-
* operation so the live target list is always used.
|
|
136
|
-
*/
|
|
137
311
|
constructor(getItems) {
|
|
138
312
|
this.#getItems = getItems;
|
|
139
313
|
}
|
|
140
|
-
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
141
314
|
get activeIndex() {
|
|
142
315
|
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
143
316
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
146
|
-
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
147
|
-
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
148
|
-
* "nothing is currently tabbable".
|
|
149
|
-
*
|
|
150
|
-
* @param index - Position of the item to make tabbable.
|
|
151
|
-
* @param options - Pass `{ focus: true }` to also move DOM focus to that item,
|
|
152
|
-
* and `items` to reuse an event-scoped collection snapshot.
|
|
153
|
-
*/
|
|
154
317
|
setActive(index, options = {}) {
|
|
155
318
|
const { focus = false } = options;
|
|
156
319
|
const items = options.items ?? this.#getItems();
|
|
@@ -161,130 +324,212 @@ var RovingTabindex = class {
|
|
|
161
324
|
}
|
|
162
325
|
};
|
|
163
326
|
|
|
164
|
-
|
|
165
|
-
|
|
327
|
+
function queryOpenDescendants(root, selector) {
|
|
328
|
+
const matches = [];
|
|
329
|
+
const roots = [root];
|
|
330
|
+
for (const scope of roots) {
|
|
331
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
332
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
333
|
+
const shadow = scope.shadowRoot;
|
|
334
|
+
if (shadow) roots.push(shadow);
|
|
335
|
+
}
|
|
336
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
337
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return matches;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
var TabindexLoan = class _TabindexLoan {
|
|
344
|
+
static #registry = sharedRegistry(
|
|
345
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
346
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
347
|
+
);
|
|
166
348
|
#value;
|
|
349
|
+
#record;
|
|
167
350
|
#lent = /* @__PURE__ */ new Set();
|
|
168
|
-
|
|
169
|
-
#beforeCache = new BeforeCacheReset(() => this.returnAll());
|
|
170
|
-
/**
|
|
171
|
-
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
172
|
-
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
173
|
-
* which a scroll region with no focusable content of its own needs.
|
|
174
|
-
*/
|
|
175
|
-
constructor(value = "-1") {
|
|
351
|
+
constructor(value, owner) {
|
|
176
352
|
this.#value = value;
|
|
353
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
354
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
177
355
|
}
|
|
178
|
-
/** Lends `element` the value; no-ops when it already carries a `tabindex`. */
|
|
179
356
|
lend(element) {
|
|
180
|
-
|
|
181
|
-
element.
|
|
357
|
+
const stack = this.#stack(element);
|
|
358
|
+
const current = element.getAttribute("tabindex");
|
|
359
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
360
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
361
|
+
);
|
|
362
|
+
const linked = stack.at(-1)?.value === current;
|
|
363
|
+
if (!held && current !== null && !linked) return;
|
|
182
364
|
this.#lent.add(element);
|
|
183
|
-
|
|
365
|
+
if (held) held.loan = this;
|
|
366
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
367
|
+
this.#mark(element, stack);
|
|
368
|
+
if (current === null || !held && current !== this.#value) {
|
|
369
|
+
element.setAttribute("tabindex", this.#value);
|
|
370
|
+
}
|
|
184
371
|
}
|
|
185
|
-
/** Takes back every loan whose value is still the one that was lent. */
|
|
186
372
|
returnAll() {
|
|
187
373
|
for (const element of this.#lent) {
|
|
188
|
-
|
|
374
|
+
const stack = this.#stack(element);
|
|
375
|
+
const hold = stack.find((held) => held.loan === this);
|
|
376
|
+
if (hold) this.#release(element, stack, hold);
|
|
189
377
|
}
|
|
190
378
|
this.#lent.clear();
|
|
191
|
-
|
|
379
|
+
}
|
|
380
|
+
reclaim(element) {
|
|
381
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
382
|
+
const stack = this.#stack(element);
|
|
383
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
384
|
+
if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
|
|
385
|
+
}
|
|
386
|
+
reclaimWithin(root) {
|
|
387
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
388
|
+
this.reclaim(element);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
#stack(element) {
|
|
392
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
393
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
394
|
+
stacks.set(element, stack);
|
|
395
|
+
return stack;
|
|
396
|
+
}
|
|
397
|
+
#seed(element) {
|
|
398
|
+
const current = element.getAttribute("tabindex");
|
|
399
|
+
const holds = [];
|
|
400
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
401
|
+
const value = element.getAttribute(record);
|
|
402
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
403
|
+
}
|
|
404
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
405
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
406
|
+
return holds;
|
|
407
|
+
}
|
|
408
|
+
#release(element, stack, hold) {
|
|
409
|
+
const current = element.getAttribute("tabindex");
|
|
410
|
+
const index = stack.indexOf(hold);
|
|
411
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
412
|
+
stack.splice(index, 1);
|
|
413
|
+
const above = stack[index];
|
|
414
|
+
if (above) above.linked &&= hold.linked;
|
|
415
|
+
this.#mark(element, stack);
|
|
416
|
+
if (!owns) return;
|
|
417
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
418
|
+
if (!below) element.removeAttribute("tabindex");
|
|
419
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
420
|
+
}
|
|
421
|
+
#mark(element, stack) {
|
|
422
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
423
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
424
|
+
for (const [name, recorded] of records) {
|
|
425
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
426
|
+
if (recorded === null) element.removeAttribute(name);
|
|
427
|
+
else element.setAttribute(name, recorded);
|
|
428
|
+
}
|
|
192
429
|
}
|
|
193
430
|
};
|
|
194
431
|
|
|
195
|
-
|
|
196
|
-
|
|
432
|
+
function targetSelector(identifier, name) {
|
|
433
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
var RatingController = class _RatingController extends Controller {
|
|
437
|
+
#numbers = new NumberValueReader();
|
|
438
|
+
#moves = new MoveCounter();
|
|
439
|
+
#move = 0;
|
|
197
440
|
static targets = ["symbol", "field"];
|
|
198
441
|
static values = {
|
|
199
442
|
value: { type: Number, default: 0 },
|
|
200
443
|
clearable: { type: Boolean, default: true },
|
|
201
444
|
readonly: { type: Boolean, default: false }
|
|
202
445
|
};
|
|
446
|
+
static valueConstraints = {
|
|
447
|
+
value: NUMBER_BOUNDS.finite
|
|
448
|
+
};
|
|
203
449
|
static actions = ["endPreview", "onKeydown", "preview", "select"];
|
|
204
450
|
static events = ["change", "reconcile"];
|
|
205
451
|
#roving = new RovingTabindex(() => this.symbolTargets);
|
|
206
|
-
#rootRole = new AttributeLease("role");
|
|
207
|
-
#symbolRole = new AttributeLease("role");
|
|
208
|
-
#symbolAriaHidden = new AttributeLease("aria-hidden");
|
|
209
|
-
#rootTabindex = new TabindexLoan();
|
|
210
|
-
#repaint = new
|
|
211
|
-
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
452
|
+
#rootRole = new AttributeLease("role", this.identifier);
|
|
453
|
+
#symbolRole = new AttributeLease("role", this.identifier);
|
|
454
|
+
#symbolAriaHidden = new AttributeLease("aria-hidden", this.identifier);
|
|
455
|
+
#rootTabindex = new TabindexLoan("-1", this.identifier);
|
|
456
|
+
#repaint = new MorphRenderWatcher(() => this.#reconcileScale());
|
|
212
457
|
#connected = false;
|
|
213
458
|
#rescuedFocus = false;
|
|
214
|
-
/** The rating shown last, which the next move is measured from. */
|
|
215
459
|
#shown = 0;
|
|
216
|
-
/** Reflects declarative state without announcing an initial user change. */
|
|
217
460
|
connect() {
|
|
218
|
-
this.#
|
|
219
|
-
this.#
|
|
220
|
-
this.#shown = this.#normalize(this
|
|
461
|
+
this.#rootTabindex.reclaim(this.element);
|
|
462
|
+
this.#repaint.observe(this.element);
|
|
463
|
+
this.#shown = this.#normalize(this.#safeValue);
|
|
221
464
|
this.#apply(this.#shown, { focus: false });
|
|
222
465
|
this.#connected = true;
|
|
223
466
|
}
|
|
224
|
-
/** Drops a queued reconciliation and hands every borrowed attribute back. */
|
|
225
467
|
disconnect() {
|
|
226
468
|
this.#connected = false;
|
|
227
|
-
this.#repaint.
|
|
228
|
-
this.#beforeCache.deactivate();
|
|
469
|
+
this.#repaint.disconnect();
|
|
229
470
|
this.#releaseReadonly();
|
|
230
471
|
}
|
|
231
|
-
/** Removes a runtime-added symbol's authored Tab stop before the batch repaint. */
|
|
232
472
|
symbolTargetConnected(symbol) {
|
|
233
473
|
if (this.#connected === false) return;
|
|
234
474
|
symbol.tabIndex = -1;
|
|
235
475
|
this.#repaint.schedule();
|
|
236
476
|
}
|
|
237
|
-
/** Releases readonly ownership and reconciles the remaining DOM-ordered scale. */
|
|
238
477
|
symbolTargetDisconnected(symbol) {
|
|
239
478
|
this.#symbolRole.return(symbol);
|
|
240
479
|
this.#symbolAriaHidden.return(symbol);
|
|
241
480
|
this.#repaint.schedule();
|
|
242
481
|
}
|
|
243
|
-
/** Reflects into a hidden field added or replaced after connection. */
|
|
244
482
|
fieldTargetConnected() {
|
|
245
483
|
this.#repaint.schedule();
|
|
246
484
|
}
|
|
247
|
-
/** Reconciles after a hidden field is removed or replaced. */
|
|
248
485
|
fieldTargetDisconnected() {
|
|
249
486
|
this.#repaint.schedule();
|
|
250
487
|
}
|
|
251
|
-
/** Repaints when application code or a Turbo morph changes `value`. */
|
|
252
488
|
valueValueChanged() {
|
|
253
489
|
this.#repaint.schedule();
|
|
254
490
|
}
|
|
255
|
-
/** Repaints when application code changes whether value 0 is permitted. */
|
|
256
491
|
clearableValueChanged() {
|
|
257
492
|
this.#repaint.schedule();
|
|
258
493
|
}
|
|
259
|
-
/** Repaints when application code enters or leaves the readonly snapshot. */
|
|
260
494
|
readonlyValueChanged() {
|
|
261
495
|
this.#repaint.schedule();
|
|
262
496
|
}
|
|
263
|
-
|
|
264
|
-
select(event) {
|
|
497
|
+
select(source) {
|
|
265
498
|
if (this.readonlyValue) return;
|
|
266
|
-
const
|
|
499
|
+
const { event, host, origin, reason } = actionSource(source);
|
|
500
|
+
const symbol = host?.closest(targetSelector(this.identifier, "symbol"));
|
|
501
|
+
if (!symbol || !this.symbolTargets.includes(symbol)) return;
|
|
502
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
503
|
+
return;
|
|
504
|
+
const moveFocus = event !== null || this.element.contains(document.activeElement);
|
|
505
|
+
const ordinal = this.#symbolOrdinal(symbol);
|
|
267
506
|
if (ordinal === null) return;
|
|
268
|
-
const current = this.#normalize(this
|
|
269
|
-
this.#commit(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
507
|
+
const current = this.#normalize(this.#safeValue);
|
|
508
|
+
this.#commit(
|
|
509
|
+
this.clearableValue && ordinal === current ? 0 : ordinal,
|
|
510
|
+
{
|
|
511
|
+
focus: ordinal === current && moveFocus
|
|
512
|
+
},
|
|
513
|
+
reason
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
preview(source) {
|
|
275
517
|
if (this.readonlyValue) return;
|
|
276
|
-
const
|
|
518
|
+
const { host, origin } = actionSource(source);
|
|
519
|
+
const symbol = host?.closest(targetSelector(this.identifier, "symbol"));
|
|
520
|
+
if (!symbol || !this.symbolTargets.includes(symbol)) return;
|
|
521
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
522
|
+
return;
|
|
523
|
+
const ordinal = this.#symbolOrdinal(symbol);
|
|
277
524
|
if (ordinal !== null) this.#setFillRange(ordinal);
|
|
278
525
|
}
|
|
279
|
-
/** Restores the fill range after hover or focus leaves a symbol. */
|
|
280
526
|
endPreview() {
|
|
281
527
|
if (this.readonlyValue) return;
|
|
282
|
-
this.#setFillRange(this.#normalize(this
|
|
528
|
+
this.#setFillRange(this.#normalize(this.#safeValue));
|
|
283
529
|
}
|
|
284
|
-
/** Arrow/Home/End/Space/Delete keyboard control, clamped without wrapping. */
|
|
285
530
|
onKeydown(event) {
|
|
286
531
|
if (event.defaultPrevented || isReservedArrowChord(event) || this.readonlyValue) return;
|
|
287
|
-
const current = this.#normalize(this
|
|
532
|
+
const current = this.#normalize(this.#safeValue);
|
|
288
533
|
let next = null;
|
|
289
534
|
const rtl = isRtl(this.element);
|
|
290
535
|
switch (event.key) {
|
|
@@ -318,69 +563,54 @@ var RatingController = class extends Controller {
|
|
|
318
563
|
event.preventDefault();
|
|
319
564
|
this.#commit(next, { focus: true });
|
|
320
565
|
}
|
|
321
|
-
/**
|
|
322
|
-
* Repaints one settled target/Value mutation batch and reports a rating shown
|
|
323
|
-
* that moved from the one shown before it.
|
|
324
|
-
*
|
|
325
|
-
* @stimeoRenderRoot
|
|
326
|
-
*/
|
|
327
566
|
#reconcileScale() {
|
|
328
|
-
const value = this.#normalize(this
|
|
567
|
+
const value = this.#normalize(this.#safeValue);
|
|
329
568
|
const previous = this.#shown;
|
|
330
569
|
this.#shown = value;
|
|
570
|
+
const move = this.#move;
|
|
331
571
|
this.#apply(value, { focus: false });
|
|
572
|
+
if (!this.#moves.isLatest(move)) return;
|
|
332
573
|
if (value !== previous) this.dispatch("reconcile", { detail: { value } });
|
|
333
574
|
}
|
|
334
|
-
|
|
335
|
-
* Applies one user operation and emits only when the rating shown changes.
|
|
336
|
-
*
|
|
337
|
-
* The user's value is written into `value` before anything reports it, so a
|
|
338
|
-
* listener of the field's native `change` already reads it there, and the pass
|
|
339
|
-
* that the write starts finds nothing left to report.
|
|
340
|
-
*/
|
|
341
|
-
#commit(raw, { focus }) {
|
|
575
|
+
#commit(raw, { focus }, reason = "user") {
|
|
342
576
|
const previous = this.#shown;
|
|
343
577
|
const value = this.#normalize(raw);
|
|
344
578
|
if (!Object.is(this.valueValue, value)) this.valueValue = value;
|
|
345
579
|
this.#shown = value;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
580
|
+
if (value !== previous) this.#move = this.#moves.record();
|
|
581
|
+
const move = this.#move;
|
|
582
|
+
const field = this.#apply(value, { focus });
|
|
583
|
+
if (!this.#moves.isLatest(move)) return;
|
|
584
|
+
if (field) commitField(field);
|
|
585
|
+
if (!this.#moves.isLatest(move)) return;
|
|
586
|
+
if (value !== previous) this.dispatch("change", { detail: { value, reason } });
|
|
587
|
+
}
|
|
588
|
+
#apply(value, { focus }) {
|
|
589
|
+
const move = this.#move;
|
|
351
590
|
this.symbolTargets.forEach((symbol, index) => {
|
|
352
591
|
symbol.setAttribute("aria-checked", value > 0 && index + 1 === value ? "true" : "false");
|
|
353
592
|
});
|
|
593
|
+
const field = this.hasFieldTarget ? this.fieldTarget : null;
|
|
594
|
+
const moved = field && writeField(field, String(value));
|
|
595
|
+
this.#setFillRange(value);
|
|
354
596
|
if (this.readonlyValue) {
|
|
355
597
|
this.#applyReadonly();
|
|
356
598
|
} else {
|
|
357
599
|
const returning = this.#releaseReadonly();
|
|
358
600
|
this.#roving.setActive(value > 0 ? value - 1 : 0, { focus: focus || returning });
|
|
359
601
|
}
|
|
360
|
-
this.#
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
/** Mirrors `value` into the optional form field, reporting only a user's move. */
|
|
364
|
-
#mirrorField(value, notify) {
|
|
365
|
-
if (!this.hasFieldTarget) return;
|
|
366
|
-
if (writeField(this.fieldTarget, String(value)) && notify) commitField(this.fieldTarget);
|
|
602
|
+
if (this.#moves.isLatest(move)) this.#setFillRange(value);
|
|
603
|
+
return moved ? field : null;
|
|
367
604
|
}
|
|
368
|
-
/** Marks the first `range` symbols with the consumer-owned fill hook. */
|
|
369
605
|
#setFillRange(range) {
|
|
370
606
|
this.symbolTargets.forEach((symbol, index) => {
|
|
371
607
|
symbol.toggleAttribute("data-rating-hover", range > 0 && index < range);
|
|
372
608
|
});
|
|
373
609
|
}
|
|
374
|
-
/**
|
|
375
|
-
* Temporarily turns the radiogroup into a non-interactive image snapshot.
|
|
376
|
-
*
|
|
377
|
-
* Each lease is returned before it is taken again, so a value the consumer
|
|
378
|
-
* wrote while readonly becomes the value the lease restores on release. The
|
|
379
|
-
* return is a no-op on an attribute still holding this controller's own
|
|
380
|
-
* write, which is the ordinary case.
|
|
381
|
-
*/
|
|
382
610
|
#applyReadonly() {
|
|
611
|
+
const move = this.#move;
|
|
383
612
|
this.#rescueFocus();
|
|
613
|
+
if (!this.#moves.isLatest(move)) return;
|
|
384
614
|
this.#rootRole.return(this.element);
|
|
385
615
|
this.#rootRole.write(this.element, "img");
|
|
386
616
|
for (const symbol of this.symbolTargets) {
|
|
@@ -391,69 +621,50 @@ var RatingController = class extends Controller {
|
|
|
391
621
|
}
|
|
392
622
|
this.#roving.setActive(-1);
|
|
393
623
|
}
|
|
394
|
-
/**
|
|
395
|
-
* Hands every readonly borrowing back before Turbo clones the page.
|
|
396
|
-
*
|
|
397
|
-
* The snapshot is taken while the controller is still connected, so an
|
|
398
|
-
* element left as `role="img"` with hidden symbols is what a restored page
|
|
399
|
-
* connects against — and that markup would be read as the authored one,
|
|
400
|
-
* leaving no way back to the radiogroup. Rewinding first keeps the cached
|
|
401
|
-
* copy identical to what the consumer wrote.
|
|
402
|
-
*/
|
|
403
|
-
#rewindForCache() {
|
|
404
|
-
this.#releaseReadonly();
|
|
405
|
-
const value = this.#normalize(this.valueValue);
|
|
406
|
-
this.#roving.setActive(value > 0 ? value - 1 : 0);
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Lands focus on the root before the symbols leave the accessibility tree.
|
|
410
|
-
*
|
|
411
|
-
* A symbol holding focus when readonly begins would keep it while losing its
|
|
412
|
-
* role and gaining `aria-hidden`, stranding the user on a node no longer in
|
|
413
|
-
* the tree. The root is the one element that survives the transition named:
|
|
414
|
-
* it carries the consumer's accessible name under `role="img"`.
|
|
415
|
-
*/
|
|
416
624
|
#rescueFocus() {
|
|
417
625
|
const active = document.activeElement;
|
|
418
626
|
if (!(active instanceof HTMLElement) || active === this.element) return;
|
|
419
627
|
if (!this.element.contains(active)) return;
|
|
420
628
|
this.#rootTabindex.lend(this.element);
|
|
421
|
-
this.element.focus();
|
|
422
629
|
this.#rescuedFocus = true;
|
|
630
|
+
this.element.focus();
|
|
423
631
|
}
|
|
424
|
-
/**
|
|
425
|
-
* Restores authored roles and visibility after leaving readonly mode.
|
|
426
|
-
*
|
|
427
|
-
* @returns whether focus is standing on the root because {@link #rescueFocus}
|
|
428
|
-
* put it there, and therefore belongs back on the Tab stop.
|
|
429
|
-
*/
|
|
430
632
|
#releaseReadonly() {
|
|
431
633
|
const returning = this.#rescuedFocus && document.activeElement === this.element;
|
|
432
634
|
this.#rescuedFocus = false;
|
|
433
635
|
this.#rootRole.return(this.element);
|
|
636
|
+
for (const symbol of this.symbolTargets) {
|
|
637
|
+
this.#symbolRole.return(symbol);
|
|
638
|
+
this.#symbolAriaHidden.return(symbol);
|
|
639
|
+
}
|
|
434
640
|
this.#symbolRole.returnAll();
|
|
435
641
|
this.#symbolAriaHidden.returnAll();
|
|
436
642
|
this.#rootTabindex.returnAll();
|
|
437
643
|
return returning;
|
|
438
644
|
}
|
|
439
|
-
/** Normalizes a raw value to an integer ordinal in the live DOM range. */
|
|
440
645
|
#normalize(raw) {
|
|
441
646
|
const maximum = this.symbolTargets.length;
|
|
442
|
-
const ordinal =
|
|
647
|
+
const ordinal = Math.round(raw);
|
|
443
648
|
return Math.min(maximum, Math.max(this.#minValue, ordinal));
|
|
444
649
|
}
|
|
445
|
-
/** Lowest selectable value: 0 when clearable, otherwise 1. */
|
|
446
650
|
get #minValue() {
|
|
447
651
|
return this.clearableValue ? 0 : 1;
|
|
448
652
|
}
|
|
449
|
-
/** Returns a target's 1-based position, or null when the action host is invalid. */
|
|
450
653
|
#symbolOrdinal(target) {
|
|
451
654
|
const targets = this.symbolTargets;
|
|
452
655
|
const index = targets.indexOf(target);
|
|
453
656
|
return index < 0 ? null : index + 1;
|
|
454
657
|
}
|
|
658
|
+
get #safeValue() {
|
|
659
|
+
return this.#numbers.read(
|
|
660
|
+
this,
|
|
661
|
+
"value",
|
|
662
|
+
this.valueValue,
|
|
663
|
+
_RatingController.values.value.default,
|
|
664
|
+
_RatingController.valueConstraints.value
|
|
665
|
+
);
|
|
666
|
+
}
|
|
455
667
|
};
|
|
456
668
|
|
|
457
669
|
export { RatingController };
|
|
458
|
-
//# sourceMappingURL=rating_controller.js.map
|
|
459
670
|
//# sourceMappingURL=rating_controller.js.map
|