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,8 +1,27 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
4
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
5
|
+
function stateReasonFor(event) {
|
|
6
|
+
if (!event) return "api";
|
|
7
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
8
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
9
|
+
return "user";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function actionSource(source) {
|
|
13
|
+
if (source instanceof HTMLElement) {
|
|
14
|
+
return { event: null, host: source, origin: source, reason: "api" };
|
|
15
|
+
}
|
|
16
|
+
const event = source ?? null;
|
|
17
|
+
return {
|
|
18
|
+
event,
|
|
19
|
+
host: event?.currentTarget instanceof Element ? event.currentTarget : null,
|
|
20
|
+
origin: event?.target instanceof Element ? event.target : null,
|
|
21
|
+
reason: stateReasonFor(event)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
4
24
|
|
|
5
|
-
// src/utils/focus_candidate.ts
|
|
6
25
|
function inheritsFieldsetDisabled(control) {
|
|
7
26
|
let fieldset = control.closest("fieldset[disabled]");
|
|
8
27
|
while (fieldset) {
|
|
@@ -12,35 +31,52 @@ function inheritsFieldsetDisabled(control) {
|
|
|
12
31
|
}
|
|
13
32
|
return false;
|
|
14
33
|
}
|
|
34
|
+
function unslotted(element) {
|
|
35
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
36
|
+
}
|
|
37
|
+
function flatTreeParent(element) {
|
|
38
|
+
if (unslotted(element)) return null;
|
|
39
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
40
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
41
|
+
return parent instanceof Element ? parent : null;
|
|
42
|
+
}
|
|
15
43
|
function canTakeFocus(element) {
|
|
16
|
-
|
|
44
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
45
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
46
|
+
}
|
|
17
47
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
18
48
|
if (!("disabled" in element)) return true;
|
|
19
49
|
if (element.disabled) return false;
|
|
20
50
|
return !inheritsFieldsetDisabled(element);
|
|
21
51
|
}
|
|
22
52
|
|
|
23
|
-
|
|
53
|
+
var ListenerSet = class {
|
|
54
|
+
#abort = new AbortController();
|
|
55
|
+
add(target, type, handler, options) {
|
|
56
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
57
|
+
}
|
|
58
|
+
dispose() {
|
|
59
|
+
this.#abort.abort();
|
|
60
|
+
this.#abort = new AbortController();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
24
64
|
var MicrotaskCoalescer = class {
|
|
25
65
|
#run;
|
|
26
66
|
#queued = false;
|
|
27
67
|
#active = false;
|
|
28
68
|
#generation = 0;
|
|
29
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
30
69
|
constructor(run) {
|
|
31
70
|
this.#run = run;
|
|
32
71
|
}
|
|
33
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
34
72
|
activate() {
|
|
35
73
|
this.#active = true;
|
|
36
74
|
}
|
|
37
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
38
75
|
cancel() {
|
|
39
76
|
this.#active = false;
|
|
40
77
|
this.#queued = false;
|
|
41
78
|
this.#generation += 1;
|
|
42
79
|
}
|
|
43
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
44
80
|
schedule() {
|
|
45
81
|
if (!this.#active || this.#queued) return;
|
|
46
82
|
this.#queued = true;
|
|
@@ -53,171 +89,304 @@ var MicrotaskCoalescer = class {
|
|
|
53
89
|
}
|
|
54
90
|
};
|
|
55
91
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
64
|
-
#rewind;
|
|
65
|
-
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
66
|
-
constructor(rewind) {
|
|
67
|
-
this.#rewind = rewind;
|
|
92
|
+
var MorphRenderWatcher = class {
|
|
93
|
+
#listeners = new ListenerSet();
|
|
94
|
+
#pass;
|
|
95
|
+
#element = null;
|
|
96
|
+
#onMorph = () => this.#pass.schedule();
|
|
97
|
+
constructor(run) {
|
|
98
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
68
99
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
100
|
+
observe(element) {
|
|
101
|
+
if (this.#element === element) return;
|
|
102
|
+
this.disconnect();
|
|
103
|
+
this.#element = element;
|
|
104
|
+
this.#pass.activate();
|
|
105
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
76
106
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
107
|
+
schedule() {
|
|
108
|
+
this.#pass.schedule();
|
|
109
|
+
}
|
|
110
|
+
disconnect() {
|
|
111
|
+
this.#listeners.dispose();
|
|
112
|
+
this.#pass.cancel();
|
|
113
|
+
this.#element = null;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
var MoveCounter = class {
|
|
118
|
+
#sequence = 0;
|
|
119
|
+
record() {
|
|
120
|
+
this.#sequence += 1;
|
|
121
|
+
return this.#sequence;
|
|
122
|
+
}
|
|
123
|
+
isLatest(token) {
|
|
124
|
+
return token === this.#sequence;
|
|
82
125
|
}
|
|
83
126
|
};
|
|
84
127
|
|
|
85
|
-
|
|
86
|
-
|
|
128
|
+
var NUMBER_BOUNDS = {
|
|
129
|
+
finite: { finite: true }};
|
|
130
|
+
function matchesNumberBounds(value, bounds) {
|
|
131
|
+
if (!Number.isFinite(value)) {
|
|
132
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
133
|
+
if (direction === null) return false;
|
|
134
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
135
|
+
}
|
|
136
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
137
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
138
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
139
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
140
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readNumber(raw, fallback, bounds) {
|
|
145
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
var NumberValueReader = class {
|
|
149
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
150
|
+
read(owner, name, raw, fallback, bounds) {
|
|
151
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
152
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
153
|
+
this.#lastRejected.delete(name);
|
|
154
|
+
return resolved;
|
|
155
|
+
}
|
|
156
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
157
|
+
const literal = owner.element.getAttribute(attribute);
|
|
158
|
+
if (literal === null) {
|
|
159
|
+
this.#lastRejected.delete(name);
|
|
160
|
+
return resolved;
|
|
161
|
+
}
|
|
162
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
163
|
+
this.#lastRejected.set(name, literal);
|
|
164
|
+
console.warn(
|
|
165
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return resolved;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
function queryOpenDescendants(root, selector) {
|
|
173
|
+
const matches = [];
|
|
174
|
+
const roots = [root];
|
|
175
|
+
for (const scope of roots) {
|
|
176
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
177
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
178
|
+
const shadow = scope.shadowRoot;
|
|
179
|
+
if (shadow) roots.push(shadow);
|
|
180
|
+
}
|
|
181
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
182
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return matches;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function sharedRegistry(key, create) {
|
|
189
|
+
const symbol = Symbol.for(key);
|
|
190
|
+
const scope = globalThis;
|
|
191
|
+
const existing = scope[symbol];
|
|
192
|
+
if (existing !== void 0) return existing;
|
|
193
|
+
const registry = create();
|
|
194
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
195
|
+
return registry;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
var TabindexLoan = class _TabindexLoan {
|
|
199
|
+
static #registry = sharedRegistry(
|
|
200
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
201
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
202
|
+
);
|
|
87
203
|
#value;
|
|
204
|
+
#record;
|
|
88
205
|
#lent = /* @__PURE__ */ new Set();
|
|
89
|
-
|
|
90
|
-
#beforeCache = new BeforeCacheReset(() => this.returnAll());
|
|
91
|
-
/**
|
|
92
|
-
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
93
|
-
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
94
|
-
* which a scroll region with no focusable content of its own needs.
|
|
95
|
-
*/
|
|
96
|
-
constructor(value = "-1") {
|
|
206
|
+
constructor(value, owner) {
|
|
97
207
|
this.#value = value;
|
|
208
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
209
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
98
210
|
}
|
|
99
|
-
/** Lends `element` the value; no-ops when it already carries a `tabindex`. */
|
|
100
211
|
lend(element) {
|
|
101
|
-
|
|
102
|
-
element.
|
|
212
|
+
const stack = this.#stack(element);
|
|
213
|
+
const current = element.getAttribute("tabindex");
|
|
214
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
215
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
216
|
+
);
|
|
217
|
+
const linked = stack.at(-1)?.value === current;
|
|
218
|
+
if (!held && current !== null && !linked) return;
|
|
103
219
|
this.#lent.add(element);
|
|
104
|
-
|
|
220
|
+
if (held) held.loan = this;
|
|
221
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
222
|
+
this.#mark(element, stack);
|
|
223
|
+
if (current === null || !held && current !== this.#value) {
|
|
224
|
+
element.setAttribute("tabindex", this.#value);
|
|
225
|
+
}
|
|
105
226
|
}
|
|
106
|
-
/** Takes back every loan whose value is still the one that was lent. */
|
|
107
227
|
returnAll() {
|
|
108
228
|
for (const element of this.#lent) {
|
|
109
|
-
|
|
229
|
+
const stack = this.#stack(element);
|
|
230
|
+
const hold = stack.find((held) => held.loan === this);
|
|
231
|
+
if (hold) this.#release(element, stack, hold);
|
|
110
232
|
}
|
|
111
233
|
this.#lent.clear();
|
|
112
|
-
|
|
234
|
+
}
|
|
235
|
+
reclaim(element) {
|
|
236
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
237
|
+
const stack = this.#stack(element);
|
|
238
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
239
|
+
if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
|
|
240
|
+
}
|
|
241
|
+
reclaimWithin(root) {
|
|
242
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
243
|
+
this.reclaim(element);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
#stack(element) {
|
|
247
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
248
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
249
|
+
stacks.set(element, stack);
|
|
250
|
+
return stack;
|
|
251
|
+
}
|
|
252
|
+
#seed(element) {
|
|
253
|
+
const current = element.getAttribute("tabindex");
|
|
254
|
+
const holds = [];
|
|
255
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
256
|
+
const value = element.getAttribute(record);
|
|
257
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
258
|
+
}
|
|
259
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
260
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
261
|
+
return holds;
|
|
262
|
+
}
|
|
263
|
+
#release(element, stack, hold) {
|
|
264
|
+
const current = element.getAttribute("tabindex");
|
|
265
|
+
const index = stack.indexOf(hold);
|
|
266
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
267
|
+
stack.splice(index, 1);
|
|
268
|
+
const above = stack[index];
|
|
269
|
+
if (above) above.linked &&= hold.linked;
|
|
270
|
+
this.#mark(element, stack);
|
|
271
|
+
if (!owns) return;
|
|
272
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
273
|
+
if (!below) element.removeAttribute("tabindex");
|
|
274
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
275
|
+
}
|
|
276
|
+
#mark(element, stack) {
|
|
277
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
278
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
279
|
+
for (const [name, recorded] of records) {
|
|
280
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
281
|
+
if (recorded === null) element.removeAttribute(name);
|
|
282
|
+
else element.setAttribute(name, recorded);
|
|
283
|
+
}
|
|
113
284
|
}
|
|
114
285
|
};
|
|
115
286
|
|
|
116
|
-
|
|
117
|
-
|
|
287
|
+
function targetSelector(identifier, name) {
|
|
288
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
var PaginationController = class _PaginationController extends Controller {
|
|
292
|
+
#numbers = new NumberValueReader();
|
|
293
|
+
#moves = new MoveCounter();
|
|
294
|
+
#moveToken = 0;
|
|
118
295
|
static targets = ["page", "prev", "next"];
|
|
119
296
|
static values = {
|
|
120
297
|
page: { type: Number, default: 1 },
|
|
121
298
|
total: { type: Number, default: 1 }
|
|
122
299
|
};
|
|
300
|
+
static valueConstraints = {
|
|
301
|
+
page: NUMBER_BOUNDS.finite,
|
|
302
|
+
total: NUMBER_BOUNDS.finite
|
|
303
|
+
};
|
|
123
304
|
static actions = ["next", "prev", "select"];
|
|
124
305
|
static events = ["change", "reconcile"];
|
|
125
|
-
/** Marks a `disabled` this controller applied at a boundary, in its own namespace. */
|
|
126
306
|
get #boundaryAttribute() {
|
|
127
307
|
return `data-${this.identifier}-boundary-disabled`;
|
|
128
308
|
}
|
|
129
|
-
|
|
130
|
-
#
|
|
131
|
-
/**
|
|
132
|
-
* Collapses the Value and target callbacks of one mutation into one pass, and
|
|
133
|
-
* refuses the ones Stimulus delivers before `connect()`, which renders itself.
|
|
134
|
-
*/
|
|
135
|
-
#repaint = new MicrotaskCoalescer(() => this.#reconcilePage());
|
|
136
|
-
/** The page shown last, which the next move of the current page is measured from. */
|
|
309
|
+
#tabindex = new TabindexLoan("-1", this.identifier);
|
|
310
|
+
#repaint = new MorphRenderWatcher(() => this.#reconcilePage());
|
|
137
311
|
#shown = 1;
|
|
138
|
-
/** Renders the initial state from the clamped `page` and `total`. */
|
|
139
312
|
connect() {
|
|
140
|
-
this.#
|
|
313
|
+
this.#tabindex.reclaim(this.element);
|
|
314
|
+
this.#repaint.observe(this.element);
|
|
141
315
|
this.#shown = this.#page;
|
|
142
316
|
this.#render();
|
|
143
317
|
}
|
|
144
|
-
/** Drops a pending pass and reverts the one attribute added outside the state hooks. */
|
|
145
318
|
disconnect() {
|
|
146
|
-
this.#repaint.
|
|
319
|
+
this.#repaint.disconnect();
|
|
147
320
|
this.#tabindex.returnAll();
|
|
148
321
|
}
|
|
149
|
-
/** Re-renders when application code (or a Turbo morph) changes `page` at runtime. */
|
|
150
322
|
pageValueChanged() {
|
|
151
323
|
this.#repaint.schedule();
|
|
152
324
|
}
|
|
153
|
-
/** Re-renders when application code (or a Turbo morph) changes `total` at runtime. */
|
|
154
325
|
totalValueChanged() {
|
|
155
326
|
this.#repaint.schedule();
|
|
156
327
|
}
|
|
157
|
-
/** Syncs a page button appended/replaced at runtime (the consumer owns the list). */
|
|
158
328
|
pageTargetConnected() {
|
|
159
329
|
this.#repaint.schedule();
|
|
160
330
|
}
|
|
161
|
-
/** Syncs a `prev` button appended/replaced at runtime. */
|
|
162
331
|
prevTargetConnected() {
|
|
163
332
|
this.#repaint.schedule();
|
|
164
333
|
}
|
|
165
|
-
|
|
334
|
+
prevTargetDisconnected(button) {
|
|
335
|
+
this.#releaseDeparted(button, this.prevTargets);
|
|
336
|
+
this.#repaint.schedule();
|
|
337
|
+
}
|
|
166
338
|
nextTargetConnected() {
|
|
167
339
|
this.#repaint.schedule();
|
|
168
340
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
341
|
+
nextTargetDisconnected(button) {
|
|
342
|
+
this.#releaseDeparted(button, this.nextTargets);
|
|
343
|
+
this.#repaint.schedule();
|
|
344
|
+
}
|
|
345
|
+
select(source) {
|
|
346
|
+
const { host, origin, reason } = actionSource(source);
|
|
347
|
+
const button = host?.closest(targetSelector(this.identifier, "page"));
|
|
348
|
+
if (!button || !this.pageTargets.includes(button)) return;
|
|
349
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
350
|
+
return;
|
|
172
351
|
const raw = button.dataset.page;
|
|
173
352
|
if (raw === void 0 || raw.trim() === "") return;
|
|
174
353
|
const page = Number(raw);
|
|
175
354
|
if (!Number.isInteger(page)) return;
|
|
176
|
-
this.#goto(page);
|
|
355
|
+
this.#goto(page, reason);
|
|
177
356
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this.#goto(this.#page - 1);
|
|
357
|
+
prev(event) {
|
|
358
|
+
this.#goto(this.#page - 1, stateReasonFor(event));
|
|
181
359
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
this.#goto(this.#page + 1);
|
|
360
|
+
next(event) {
|
|
361
|
+
this.#goto(this.#page + 1, stateReasonFor(event));
|
|
185
362
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (!Number.isFinite(page)) return;
|
|
189
|
-
const previous = this.#page;
|
|
363
|
+
#goto(page, reason) {
|
|
364
|
+
const previous = this.#shown;
|
|
190
365
|
const target = this.#clamp(page);
|
|
191
|
-
if (target === previous) return;
|
|
366
|
+
if (target === previous && target === this.#page) return;
|
|
192
367
|
this.pageValue = target;
|
|
193
368
|
this.#shown = target;
|
|
369
|
+
const changed = target !== previous;
|
|
370
|
+
if (changed) this.#moveToken = this.#moves.record();
|
|
371
|
+
const token = this.#moveToken;
|
|
194
372
|
this.#render();
|
|
373
|
+
if (!changed || !this.#moves.isLatest(token)) return;
|
|
195
374
|
this.dispatch("change", {
|
|
196
|
-
detail: { page: target, total: this.#total, previous }
|
|
375
|
+
detail: { page: target, total: this.#total, previous, reason }
|
|
197
376
|
});
|
|
198
377
|
}
|
|
199
|
-
/**
|
|
200
|
-
* Renders one settled batch of Value and target changes, and reports a current
|
|
201
|
-
* page that moved from the one shown before as `reconcile`.
|
|
202
|
-
*/
|
|
203
378
|
#reconcilePage() {
|
|
204
379
|
const previous = this.#shown;
|
|
205
380
|
const page = this.#page;
|
|
206
381
|
this.#shown = page;
|
|
382
|
+
const token = this.#moveToken;
|
|
207
383
|
this.#render();
|
|
208
|
-
if (page !== previous) {
|
|
384
|
+
if (page !== previous && this.#moves.isLatest(token)) {
|
|
209
385
|
this.dispatch("reconcile", { detail: { page, total: this.#total, previous } });
|
|
210
386
|
}
|
|
211
387
|
}
|
|
212
|
-
/**
|
|
213
|
-
* Syncs `aria-current` on the page buttons and the prev/next `disabled` state.
|
|
214
|
-
*
|
|
215
|
-
* It reads `page` and `total` through their clamps and never writes either Value,
|
|
216
|
-
* so a declaration outside the range stays in the attributes as the page wrote it.
|
|
217
|
-
*
|
|
218
|
-
* @stimeoRenderRoot
|
|
219
|
-
*/
|
|
220
388
|
#render() {
|
|
389
|
+
const token = this.#moveToken;
|
|
221
390
|
const page = this.#page;
|
|
222
391
|
for (const button of this.pageTargets) {
|
|
223
392
|
if (Number(button.dataset.page) === page) {
|
|
@@ -230,40 +399,35 @@ var PaginationController = class extends Controller {
|
|
|
230
399
|
const next = this.hasNextTarget ? this.nextTarget : null;
|
|
231
400
|
const atStart = page <= 1;
|
|
232
401
|
const atEnd = page >= this.#total;
|
|
233
|
-
const prevStaysDisabled = this.#disabledAfter(prev, atStart);
|
|
234
402
|
const nextStaysDisabled = this.#disabledAfter(next, atEnd);
|
|
235
403
|
this.#release(prev, atStart);
|
|
236
404
|
this.#release(next, atEnd);
|
|
237
405
|
this.#disable(prev, atStart, nextStaysDisabled ? null : next);
|
|
238
|
-
this.#
|
|
406
|
+
if (!this.#moves.isLatest(token)) return;
|
|
407
|
+
this.#disable(next, atEnd, prev);
|
|
239
408
|
}
|
|
240
|
-
/** Whether a boundary button will still be `disabled` once this render applies. */
|
|
241
409
|
#disabledAfter(button, atBoundary) {
|
|
242
410
|
if (!button) return true;
|
|
243
411
|
if (atBoundary) return true;
|
|
244
412
|
return button.disabled && !this.#owns(button);
|
|
245
413
|
}
|
|
246
|
-
|
|
414
|
+
#releaseDeparted(button, targets) {
|
|
415
|
+
if (!targets.includes(button)) this.#release(button, false);
|
|
416
|
+
}
|
|
247
417
|
#release(button, atBoundary) {
|
|
248
418
|
if (!button || atBoundary || !this.#owns(button)) return;
|
|
249
419
|
button.disabled = false;
|
|
250
420
|
button.removeAttribute(this.#boundaryAttribute);
|
|
251
421
|
}
|
|
252
|
-
/**
|
|
253
|
-
* Disables a boundary button, first moving focus off it when it is the active
|
|
254
|
-
* element so disabling never strands focus.
|
|
255
|
-
*/
|
|
256
422
|
#disable(button, atBoundary, opposite) {
|
|
257
423
|
if (!button || !atBoundary) return;
|
|
258
424
|
if (button.disabled && !this.#owns(button)) return;
|
|
425
|
+
const token = this.#moveToken;
|
|
259
426
|
if (button === document.activeElement) this.#moveFocusAwayFrom(opposite);
|
|
427
|
+
if (!this.#moves.isLatest(token)) return;
|
|
260
428
|
button.disabled = true;
|
|
261
429
|
button.setAttribute(this.#boundaryAttribute, "");
|
|
262
430
|
}
|
|
263
|
-
/**
|
|
264
|
-
* Moves focus to `opposite` (already resolved to `null` when it will stay
|
|
265
|
-
* disabled), else to the current page button, else to the landmark itself.
|
|
266
|
-
*/
|
|
267
431
|
#moveFocusAwayFrom(opposite) {
|
|
268
432
|
const page = this.#page;
|
|
269
433
|
const currentPage = this.pageTargets.find(
|
|
@@ -279,26 +443,38 @@ var PaginationController = class extends Controller {
|
|
|
279
443
|
this.#tabindex.lend(this.element);
|
|
280
444
|
this.element.focus();
|
|
281
445
|
}
|
|
282
|
-
/** Total pages, normalized to a finite integer >= 1. */
|
|
283
446
|
get #total() {
|
|
284
|
-
const total = this
|
|
285
|
-
return
|
|
447
|
+
const total = this.#safeTotal;
|
|
448
|
+
return Math.max(1, Math.trunc(total));
|
|
286
449
|
}
|
|
287
|
-
/** The current page, normalized into `[1, total]`. */
|
|
288
450
|
get #page() {
|
|
289
|
-
return this.#clamp(this
|
|
451
|
+
return this.#clamp(this.#safePage);
|
|
290
452
|
}
|
|
291
|
-
/** Constrains `page` to `[1, total]`; a non-finite page falls back to page 1. */
|
|
292
453
|
#clamp(page) {
|
|
293
|
-
if (!Number.isFinite(page)) return 1;
|
|
294
454
|
return Math.min(this.#total, Math.max(1, Math.trunc(page)));
|
|
295
455
|
}
|
|
296
|
-
/** Whether the button's current `disabled` was applied by boundary control. */
|
|
297
456
|
#owns(button) {
|
|
298
457
|
return button.hasAttribute(this.#boundaryAttribute);
|
|
299
458
|
}
|
|
459
|
+
get #safePage() {
|
|
460
|
+
return this.#numbers.read(
|
|
461
|
+
this,
|
|
462
|
+
"page",
|
|
463
|
+
this.pageValue,
|
|
464
|
+
_PaginationController.values.page.default,
|
|
465
|
+
_PaginationController.valueConstraints.page
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
get #safeTotal() {
|
|
469
|
+
return this.#numbers.read(
|
|
470
|
+
this,
|
|
471
|
+
"total",
|
|
472
|
+
this.totalValue,
|
|
473
|
+
_PaginationController.values.total.default,
|
|
474
|
+
_PaginationController.valueConstraints.total
|
|
475
|
+
);
|
|
476
|
+
}
|
|
300
477
|
};
|
|
301
478
|
|
|
302
479
|
export { PaginationController };
|
|
303
|
-
//# sourceMappingURL=pagination_controller.js.map
|
|
304
480
|
//# sourceMappingURL=pagination_controller.js.map
|