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,28 +1,177 @@
|
|
|
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
|
+
function leasedAuthorValue(element, attribute) {
|
|
30
|
+
const suffix = `-${attribute}-lease`;
|
|
31
|
+
for (const name of element.getAttributeNames()) {
|
|
32
|
+
if (!name.startsWith("data-") || !name.endsWith(suffix)) continue;
|
|
33
|
+
const record = readLeaseRecord(element, name);
|
|
34
|
+
if (record) return record.original;
|
|
35
|
+
}
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
(class _AttributeLease {
|
|
39
|
+
static #registry = sharedRegistry(
|
|
40
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
41
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
42
|
+
);
|
|
43
|
+
#attribute;
|
|
44
|
+
#record;
|
|
45
|
+
#base;
|
|
46
|
+
#held = /* @__PURE__ */ new Set();
|
|
47
|
+
constructor(attribute, owner, options = {}) {
|
|
48
|
+
this.#attribute = attribute;
|
|
49
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
50
|
+
this.#base = options.base === true;
|
|
51
|
+
const names = _AttributeLease.#registry.names;
|
|
52
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
53
|
+
}
|
|
54
|
+
write(element, value) {
|
|
55
|
+
const stack = this.#stack(element);
|
|
56
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
57
|
+
this.#held.add(element);
|
|
58
|
+
const shows = !this.#covered(element, stack, hold);
|
|
59
|
+
hold.written = value;
|
|
60
|
+
this.#mark(element, stack);
|
|
61
|
+
if (shows) this.#reflect(element, value);
|
|
62
|
+
}
|
|
63
|
+
return(element) {
|
|
64
|
+
const held = this.#held.delete(element);
|
|
65
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
66
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
67
|
+
const hold = this.#take(stack);
|
|
68
|
+
if (!hold) return false;
|
|
69
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
70
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
71
|
+
const index = stack.indexOf(hold);
|
|
72
|
+
stack.splice(index, 1);
|
|
73
|
+
const above = stack[index];
|
|
74
|
+
if (above) above.linked &&= hold.linked;
|
|
75
|
+
this.#mark(element, stack);
|
|
76
|
+
if (!owns) return true;
|
|
77
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
78
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
returnAll() {
|
|
82
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
83
|
+
}
|
|
84
|
+
#stack(element) {
|
|
85
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
86
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
87
|
+
stacks.set(element, byAttribute);
|
|
88
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
89
|
+
byAttribute.set(this.#attribute, stack);
|
|
90
|
+
return stack;
|
|
91
|
+
}
|
|
92
|
+
#seed(element) {
|
|
93
|
+
const found = [];
|
|
94
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
95
|
+
const read = readLeaseRecord(element, record);
|
|
96
|
+
if (read) found.push({ ...read, record });
|
|
97
|
+
}
|
|
98
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
99
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
100
|
+
lease: null,
|
|
101
|
+
record,
|
|
102
|
+
written,
|
|
103
|
+
original,
|
|
104
|
+
linked: linked && index > 0
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
#take(stack) {
|
|
108
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
109
|
+
if (hold) hold.lease = this;
|
|
110
|
+
return hold;
|
|
111
|
+
}
|
|
112
|
+
#acquire(element, stack) {
|
|
113
|
+
const current = element.getAttribute(this.#attribute);
|
|
114
|
+
const top = stack.at(-1);
|
|
115
|
+
const bottom = stack[0];
|
|
116
|
+
const over = top?.written === current ? top : void 0;
|
|
117
|
+
const from = this.#base ? bottom : over;
|
|
118
|
+
const hold = {
|
|
119
|
+
lease: this,
|
|
120
|
+
record: this.#record,
|
|
121
|
+
written: current,
|
|
122
|
+
original: from ? from.original : current,
|
|
123
|
+
linked: !this.#base && over !== void 0
|
|
124
|
+
};
|
|
125
|
+
if (this.#base && bottom) {
|
|
126
|
+
bottom.linked = true;
|
|
127
|
+
stack.unshift(hold);
|
|
128
|
+
} else stack.push(hold);
|
|
129
|
+
return hold;
|
|
130
|
+
}
|
|
131
|
+
#covered(element, stack, hold) {
|
|
132
|
+
const current = element.getAttribute(this.#attribute);
|
|
133
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
134
|
+
}
|
|
135
|
+
#mark(element, stack) {
|
|
136
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
137
|
+
stack.forEach((hold, index) => {
|
|
138
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
139
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
140
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
141
|
+
records.set(
|
|
142
|
+
hold.record,
|
|
143
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
for (const [name, recorded] of records) {
|
|
147
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
148
|
+
if (recorded === null) element.removeAttribute(name);
|
|
149
|
+
else element.setAttribute(name, recorded);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
#reflect(element, value) {
|
|
153
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
154
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
155
|
+
else element.setAttribute(this.#attribute, value);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
4
158
|
|
|
5
|
-
// src/utils/microtask_coalescer.ts
|
|
6
159
|
var MicrotaskCoalescer = class {
|
|
7
160
|
#run;
|
|
8
161
|
#queued = false;
|
|
9
162
|
#active = false;
|
|
10
163
|
#generation = 0;
|
|
11
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
12
164
|
constructor(run) {
|
|
13
165
|
this.#run = run;
|
|
14
166
|
}
|
|
15
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
16
167
|
activate() {
|
|
17
168
|
this.#active = true;
|
|
18
169
|
}
|
|
19
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
20
170
|
cancel() {
|
|
21
171
|
this.#active = false;
|
|
22
172
|
this.#queued = false;
|
|
23
173
|
this.#generation += 1;
|
|
24
174
|
}
|
|
25
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
26
175
|
schedule() {
|
|
27
176
|
if (!this.#active || this.#queued) return;
|
|
28
177
|
this.#queued = true;
|
|
@@ -35,7 +184,52 @@ var MicrotaskCoalescer = class {
|
|
|
35
184
|
}
|
|
36
185
|
};
|
|
37
186
|
|
|
38
|
-
|
|
187
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
188
|
+
|
|
189
|
+
var NUMBER_BOUNDS = {
|
|
190
|
+
timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
|
|
191
|
+
function matchesNumberBounds(value, bounds) {
|
|
192
|
+
if (!Number.isFinite(value)) {
|
|
193
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
194
|
+
if (direction === null) return false;
|
|
195
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
196
|
+
}
|
|
197
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
198
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
199
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
200
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
201
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function readNumber(raw, fallback, bounds) {
|
|
206
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
var NumberValueReader = class {
|
|
210
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
211
|
+
read(owner, name, raw, fallback, bounds) {
|
|
212
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
213
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
214
|
+
this.#lastRejected.delete(name);
|
|
215
|
+
return resolved;
|
|
216
|
+
}
|
|
217
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
218
|
+
const literal = owner.element.getAttribute(attribute);
|
|
219
|
+
if (literal === null) {
|
|
220
|
+
this.#lastRejected.delete(name);
|
|
221
|
+
return resolved;
|
|
222
|
+
}
|
|
223
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
224
|
+
this.#lastRejected.set(name, literal);
|
|
225
|
+
console.warn(
|
|
226
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return resolved;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
39
233
|
function readLocalStorage(key) {
|
|
40
234
|
try {
|
|
41
235
|
return { ok: true, value: window.localStorage.getItem(key) };
|
|
@@ -60,43 +254,24 @@ function removeLocalStorage(key) {
|
|
|
60
254
|
}
|
|
61
255
|
}
|
|
62
256
|
|
|
63
|
-
// src/utils/safe_timeout.ts
|
|
64
257
|
var TimerRegistry = class {
|
|
65
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
66
258
|
ids = /* @__PURE__ */ new Set();
|
|
67
|
-
/**
|
|
68
|
-
* Cancels a single tracked timer.
|
|
69
|
-
*
|
|
70
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
71
|
-
* registry), so callers can clear defensively without guarding.
|
|
72
|
-
*/
|
|
73
259
|
clear(id) {
|
|
74
260
|
if (this.ids.delete(id)) {
|
|
75
261
|
this.cancel(id);
|
|
76
262
|
}
|
|
77
263
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
80
|
-
* to guarantee no timer outlives the element.
|
|
81
|
-
*/
|
|
82
264
|
clearAll() {
|
|
83
265
|
for (const id of this.ids) {
|
|
84
266
|
this.cancel(id);
|
|
85
267
|
}
|
|
86
268
|
this.ids.clear();
|
|
87
269
|
}
|
|
88
|
-
/** Number of timers currently tracked (pending). */
|
|
89
270
|
get size() {
|
|
90
271
|
return this.ids.size;
|
|
91
272
|
}
|
|
92
273
|
};
|
|
93
274
|
var SafeTimeout = class extends TimerRegistry {
|
|
94
|
-
/**
|
|
95
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
96
|
-
*
|
|
97
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
98
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
99
|
-
*/
|
|
100
275
|
set(callback, delay) {
|
|
101
276
|
const id = this.schedule(() => {
|
|
102
277
|
this.ids.delete(id);
|
|
@@ -113,7 +288,6 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
113
288
|
}
|
|
114
289
|
};
|
|
115
290
|
|
|
116
|
-
// src/utils/string_list.ts
|
|
117
291
|
function parseStringList(raw, fallback = []) {
|
|
118
292
|
const text = raw.trim();
|
|
119
293
|
if (text.length === 0) return [...fallback];
|
|
@@ -127,19 +301,16 @@ function parseStringList(raw, fallback = []) {
|
|
|
127
301
|
return parsed.filter((entry) => typeof entry === "string");
|
|
128
302
|
}
|
|
129
303
|
|
|
130
|
-
// src/utils/transient_hooks.ts
|
|
131
304
|
var TransientHooks = class {
|
|
132
305
|
#attributes;
|
|
133
306
|
constructor(options) {
|
|
134
307
|
this.#attributes = options.attributes;
|
|
135
308
|
}
|
|
136
|
-
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
137
309
|
reset(element) {
|
|
138
310
|
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
139
311
|
}
|
|
140
312
|
};
|
|
141
313
|
|
|
142
|
-
// src/controllers/persist_controller.ts
|
|
143
314
|
var TRANSIENT = new TransientHooks({ attributes: ["data-persist-restored"] });
|
|
144
315
|
var NON_VALUE_TYPES = /* @__PURE__ */ new Set(["file", "submit", "reset", "button", "image"]);
|
|
145
316
|
var SENSITIVE_TYPES = /* @__PURE__ */ new Set(["password"]);
|
|
@@ -171,15 +342,18 @@ var parsePayload = (raw) => {
|
|
|
171
342
|
return { version: 1, fields };
|
|
172
343
|
};
|
|
173
344
|
var isPersistField = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement;
|
|
174
|
-
var PersistController = class extends Controller {
|
|
345
|
+
var PersistController = class _PersistController extends Controller {
|
|
346
|
+
#numbers = new NumberValueReader();
|
|
175
347
|
static targets = ["field"];
|
|
176
348
|
static values = {
|
|
177
349
|
key: { type: String, default: "" },
|
|
178
350
|
debounce: { type: Number, default: DEFAULT_DEBOUNCE },
|
|
179
|
-
// A malformed JSON list must fall back without aborting Stimulus connection.
|
|
180
351
|
exclude: { type: String, default: "" },
|
|
181
352
|
clearOn: { type: String, default: "" }
|
|
182
353
|
};
|
|
354
|
+
static valueConstraints = {
|
|
355
|
+
debounce: NUMBER_BOUNDS.timer
|
|
356
|
+
};
|
|
183
357
|
static actions = ["clear"];
|
|
184
358
|
static events = ["restore", "save", "clear", "error"];
|
|
185
359
|
#timeouts = new SafeTimeout();
|
|
@@ -208,14 +382,13 @@ var PersistController = class extends Controller {
|
|
|
208
382
|
this.#connected = true;
|
|
209
383
|
this.#logicalKey = this.#resolveLogicalKey();
|
|
210
384
|
this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);
|
|
211
|
-
this.#knownFields = /* @__PURE__ */ new WeakSet();
|
|
212
|
-
this.#forcedRestore.clear();
|
|
213
385
|
this.element.addEventListener("input", this.#onInput);
|
|
214
386
|
this.element.addEventListener("change", this.#onInput);
|
|
215
387
|
this.#syncClearOn();
|
|
216
388
|
this.#restoreDynamic.activate();
|
|
217
389
|
this.#observer.observe(this.element, {
|
|
218
390
|
attributes: true,
|
|
391
|
+
attributeOldValue: true,
|
|
219
392
|
attributeFilter: [
|
|
220
393
|
"checked",
|
|
221
394
|
"data-controller",
|
|
@@ -240,12 +413,9 @@ var PersistController = class extends Controller {
|
|
|
240
413
|
this.#unbindClearOn();
|
|
241
414
|
this.#observer.disconnect();
|
|
242
415
|
this.#restoreDynamic.cancel();
|
|
243
|
-
this.#forcedRestore.clear();
|
|
244
|
-
this.#timeouts.clearAll();
|
|
245
416
|
TRANSIENT.reset(this.element);
|
|
246
417
|
this.#payload = null;
|
|
247
418
|
}
|
|
248
|
-
/** Drops the active draft after storage confirms the removal. */
|
|
249
419
|
clear() {
|
|
250
420
|
const logicalKey = this.#logicalKey ?? this.#resolveLogicalKey();
|
|
251
421
|
if (logicalKey === null) return;
|
|
@@ -259,19 +429,12 @@ var PersistController = class extends Controller {
|
|
|
259
429
|
this.element.removeAttribute("data-persist-restored");
|
|
260
430
|
this.dispatch("clear", { detail: { key: logicalKey } });
|
|
261
431
|
}
|
|
262
|
-
/** Rebinds the external clear event when its Value changes. */
|
|
263
432
|
clearOnValueChanged() {
|
|
264
433
|
if (this.#connected) this.#syncClearOn();
|
|
265
434
|
}
|
|
266
|
-
/** Switches storage namespaces without writing old edits under the new key. */
|
|
267
435
|
keyValueChanged() {
|
|
268
436
|
if (this.#connected) this.#switchLogicalKey();
|
|
269
437
|
}
|
|
270
|
-
/** Reschedules a pending write against the current debounce delay. */
|
|
271
|
-
debounceValueChanged() {
|
|
272
|
-
if (this.#connected && this.#pendingSave !== null) this.#scheduleSave();
|
|
273
|
-
}
|
|
274
|
-
/** Restores every field the new exclusion list makes eligible. */
|
|
275
438
|
excludeValueChanged() {
|
|
276
439
|
const previous = this.#excluded;
|
|
277
440
|
this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);
|
|
@@ -283,7 +446,6 @@ var PersistController = class extends Controller {
|
|
|
283
446
|
}
|
|
284
447
|
this.#restoreDynamic.schedule();
|
|
285
448
|
}
|
|
286
|
-
/** Schedules a debounced save. */
|
|
287
449
|
#scheduleSave() {
|
|
288
450
|
const logicalKey = this.#logicalKey;
|
|
289
451
|
if (logicalKey === null) return;
|
|
@@ -294,21 +456,19 @@ var PersistController = class extends Controller {
|
|
|
294
456
|
}, this.#debounceDelay);
|
|
295
457
|
this.#pendingSave = { id, logicalKey };
|
|
296
458
|
}
|
|
297
|
-
/** Cancels the currently pending save, if any. */
|
|
298
459
|
#cancelPendingSave() {
|
|
299
460
|
const pending = this.#pendingSave;
|
|
300
461
|
if (pending === null) return;
|
|
301
462
|
this.#timeouts.clear(pending.id);
|
|
302
463
|
this.#pendingSave = null;
|
|
303
464
|
}
|
|
304
|
-
/** Flushes a pending edit through the same success/error event path as a timer. */
|
|
305
465
|
#flushPendingSave() {
|
|
306
466
|
const pending = this.#pendingSave;
|
|
307
467
|
if (pending === null) return;
|
|
308
468
|
this.#cancelPendingSave();
|
|
309
469
|
this.#save(pending.logicalKey);
|
|
470
|
+
this.#cancelPendingSave();
|
|
310
471
|
}
|
|
311
|
-
/** Writes the active payload and emits `save` only after storage succeeds. */
|
|
312
472
|
#save(logicalKey) {
|
|
313
473
|
const payload = this.#serialize();
|
|
314
474
|
const result = writeLocalStorage(this.#storageKey(logicalKey), JSON.stringify(payload));
|
|
@@ -320,7 +480,6 @@ var PersistController = class extends Controller {
|
|
|
320
480
|
this.dispatch("save", { detail: { key: logicalKey } });
|
|
321
481
|
return true;
|
|
322
482
|
}
|
|
323
|
-
/** Serializes every persistable field into a versioned, typed payload. */
|
|
324
483
|
#serialize() {
|
|
325
484
|
const fields = [];
|
|
326
485
|
for (const { field, key } of this.#fieldEntries()) {
|
|
@@ -339,17 +498,13 @@ var PersistController = class extends Controller {
|
|
|
339
498
|
}
|
|
340
499
|
return { version: 1, fields };
|
|
341
500
|
}
|
|
342
|
-
/** Reads, validates, and restores the active namespace. */
|
|
343
501
|
#loadActiveDraft() {
|
|
344
502
|
TRANSIENT.reset(this.element);
|
|
345
503
|
this.#payload = null;
|
|
346
504
|
this.#knownFields = /* @__PURE__ */ new WeakSet();
|
|
347
505
|
this.#forcedRestore.clear();
|
|
348
506
|
const logicalKey = this.#logicalKey;
|
|
349
|
-
if (logicalKey === null)
|
|
350
|
-
this.#markCurrentFieldsKnown();
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
507
|
+
if (logicalKey === null) return;
|
|
353
508
|
const result = readLocalStorage(this.#storageKey(logicalKey));
|
|
354
509
|
if (!result.ok) {
|
|
355
510
|
this.#dispatchError(logicalKey, "read", "unavailable");
|
|
@@ -371,7 +526,6 @@ var PersistController = class extends Controller {
|
|
|
371
526
|
this.#payload = payload;
|
|
372
527
|
this.#restoreEntries(null);
|
|
373
528
|
}
|
|
374
|
-
/** Restores fields inserted or made eligible after the initial connection. */
|
|
375
529
|
#restoreDynamicFields() {
|
|
376
530
|
const selected = new Set(this.#forcedRestore);
|
|
377
531
|
this.#forcedRestore.clear();
|
|
@@ -380,7 +534,6 @@ var PersistController = class extends Controller {
|
|
|
380
534
|
}
|
|
381
535
|
this.#restoreEntries(selected);
|
|
382
536
|
}
|
|
383
|
-
/** Applies the active payload to all fields, or only the supplied candidates. */
|
|
384
537
|
#restoreEntries(selected) {
|
|
385
538
|
const entries = this.#fieldEntries();
|
|
386
539
|
for (const { field } of entries) this.#knownFields.add(field);
|
|
@@ -413,7 +566,6 @@ var PersistController = class extends Controller {
|
|
|
413
566
|
this.dispatch("restore", { detail: { key: this.#logicalKey } });
|
|
414
567
|
}
|
|
415
568
|
}
|
|
416
|
-
/** Applies a type-compatible stored value without coercion. */
|
|
417
569
|
#applyValue(field, value) {
|
|
418
570
|
if (field instanceof HTMLInputElement && field.type === "checkbox") {
|
|
419
571
|
if (typeof value !== "boolean") return false;
|
|
@@ -439,16 +591,11 @@ var PersistController = class extends Controller {
|
|
|
439
591
|
field.value = value;
|
|
440
592
|
return true;
|
|
441
593
|
}
|
|
442
|
-
/** Every control this instance owns, before the exclusion list narrows them. */
|
|
443
594
|
#candidateFields() {
|
|
444
595
|
return this.hasFieldTarget ? this.fieldTargets.filter(isPersistField) : Array.from(this.element.querySelectorAll("input, textarea, select")).filter(
|
|
445
596
|
(field) => this.#ownsField(field)
|
|
446
597
|
);
|
|
447
598
|
}
|
|
448
|
-
/**
|
|
449
|
-
* Pairs persistable fields with DOM-order occurrence keys. Every HTML radio
|
|
450
|
-
* group (same name and form owner) occupies one shared occurrence slot.
|
|
451
|
-
*/
|
|
452
599
|
#fieldEntries() {
|
|
453
600
|
const entries = [];
|
|
454
601
|
const eligible = [];
|
|
@@ -480,26 +627,22 @@ var PersistController = class extends Controller {
|
|
|
480
627
|
}
|
|
481
628
|
return entries;
|
|
482
629
|
}
|
|
483
|
-
/** Whether a field carries a restorable, non-excluded value. */
|
|
484
630
|
#persistable(field, excluded) {
|
|
485
631
|
if (NON_VALUE_TYPES.has(field.type) || SENSITIVE_TYPES.has(field.type)) return false;
|
|
632
|
+
if (SENSITIVE_TYPES.has(leasedAuthorValue(field, "type") ?? "")) return false;
|
|
486
633
|
if (excluded.includes(field.type)) return false;
|
|
487
634
|
if (field.name.length > 0 && excluded.includes(field.name)) return false;
|
|
488
635
|
return true;
|
|
489
636
|
}
|
|
490
|
-
/** A stable storage sub-key for a field (its name, else id). */
|
|
491
637
|
#keyOf(field) {
|
|
492
638
|
return field.name || field.id || null;
|
|
493
639
|
}
|
|
494
|
-
/** Whether this instance, rather than a nested Persist host, owns a field. */
|
|
495
640
|
#ownsField(field) {
|
|
496
641
|
return field.closest(`[data-controller~="${this.identifier}"]`) === this.element;
|
|
497
642
|
}
|
|
498
|
-
/** Marks all currently eligible fields without applying a payload. */
|
|
499
643
|
#markCurrentFieldsKnown() {
|
|
500
644
|
for (const { field } of this.#fieldEntries()) this.#knownFields.add(field);
|
|
501
645
|
}
|
|
502
|
-
/** Switches namespace after flushing the old namespace's pending edit. */
|
|
503
646
|
#switchLogicalKey() {
|
|
504
647
|
const next = this.#resolveLogicalKey();
|
|
505
648
|
if (next === this.#logicalKey) return;
|
|
@@ -507,7 +650,6 @@ var PersistController = class extends Controller {
|
|
|
507
650
|
this.#logicalKey = next;
|
|
508
651
|
this.#loadActiveDraft();
|
|
509
652
|
}
|
|
510
|
-
/** Synchronizes the exact event name owned by the `clearOn` Value. */
|
|
511
653
|
#syncClearOn() {
|
|
512
654
|
const next = this.#validEventName(this.clearOnValue);
|
|
513
655
|
if (next === this.#registeredClearOn) return;
|
|
@@ -516,52 +658,47 @@ var PersistController = class extends Controller {
|
|
|
516
658
|
this.element.addEventListener(next, this.#onClearEvent);
|
|
517
659
|
this.#registeredClearOn = next;
|
|
518
660
|
}
|
|
519
|
-
/** Removes the event listener using the name that was actually registered. */
|
|
520
661
|
#unbindClearOn() {
|
|
521
662
|
if (this.#registeredClearOn === null) return;
|
|
522
663
|
this.element.removeEventListener(this.#registeredClearOn, this.#onClearEvent);
|
|
523
664
|
this.#registeredClearOn = null;
|
|
524
665
|
}
|
|
525
|
-
/** Returns a non-whitespace event type, or null when the hook is disabled. */
|
|
526
666
|
#validEventName(value) {
|
|
527
667
|
const name = value.trim();
|
|
528
668
|
return name.length > 0 && !/\s/.test(name) ? name : null;
|
|
529
669
|
}
|
|
530
|
-
/**
|
|
531
|
-
* Collects dynamic controls and select-option changes into one restore pass.
|
|
532
|
-
*
|
|
533
|
-
* @stimeoRuntimeOnly `key` decides whether an id change moves the storage namespace; it decides
|
|
534
|
-
* nothing shown.
|
|
535
|
-
*/
|
|
536
670
|
#onMutations(records) {
|
|
537
671
|
let rootIdChanged = false;
|
|
672
|
+
const ownershipBefore = /* @__PURE__ */ new Map();
|
|
538
673
|
for (const record of records) {
|
|
539
674
|
if (record.type === "attributes") {
|
|
540
675
|
if (record.target === this.element && record.attributeName === "id") {
|
|
541
676
|
rootIdChanged = true;
|
|
542
677
|
}
|
|
543
|
-
|
|
678
|
+
if (record.attributeName === OWNERSHIP_ATTRIBUTE) {
|
|
679
|
+
const element = record.target;
|
|
680
|
+
if (!ownershipBefore.has(element)) ownershipBefore.set(element, record.oldValue);
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
this.#collectControlCandidate(record.target);
|
|
544
684
|
continue;
|
|
545
685
|
}
|
|
546
686
|
for (const node of record.addedNodes) this.#collectRestoreCandidate(node);
|
|
547
687
|
}
|
|
688
|
+
for (const [element, before] of ownershipBefore) {
|
|
689
|
+
this.#collectOwnershipCandidate(element, before);
|
|
690
|
+
}
|
|
548
691
|
if (rootIdChanged && this.keyValue.length === 0) this.#switchLogicalKey();
|
|
549
692
|
this.#restoreDynamic.schedule();
|
|
550
693
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if (attributeName === OWNERSHIP_ATTRIBUTE && target !== this.element) {
|
|
559
|
-
this.#collectRestoreCandidate(target);
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
this.#collectControlCandidate(target);
|
|
694
|
+
#collectOwnershipCandidate(element, before) {
|
|
695
|
+
const after = element.getAttribute(OWNERSHIP_ATTRIBUTE);
|
|
696
|
+
if (this.#namesThisIdentifier(before) === this.#namesThisIdentifier(after)) return;
|
|
697
|
+
this.#collectRestoreCandidate(element);
|
|
698
|
+
}
|
|
699
|
+
#namesThisIdentifier(tokens) {
|
|
700
|
+
return (tokens ?? "").split(/\s+/).includes(this.identifier);
|
|
563
701
|
}
|
|
564
|
-
/** Adds a native control, or the select that owns a mutated option. */
|
|
565
702
|
#collectControlCandidate(node) {
|
|
566
703
|
if (isPersistField(node)) {
|
|
567
704
|
this.#forcedRestore.add(node);
|
|
@@ -572,7 +709,6 @@ var PersistController = class extends Controller {
|
|
|
572
709
|
if (select) this.#forcedRestore.add(select);
|
|
573
710
|
}
|
|
574
711
|
}
|
|
575
|
-
/** Adds every control inside an inserted or newly owned subtree. */
|
|
576
712
|
#collectRestoreCandidate(node) {
|
|
577
713
|
this.#collectControlCandidate(node);
|
|
578
714
|
if (!(node instanceof Element)) return;
|
|
@@ -580,31 +716,29 @@ var PersistController = class extends Controller {
|
|
|
580
716
|
this.#forcedRestore.add(field);
|
|
581
717
|
}
|
|
582
718
|
}
|
|
583
|
-
/**
|
|
584
|
-
* Resolves the logical key from the Value, falling back to the host id.
|
|
585
|
-
*
|
|
586
|
-
* @stimeoRuntimeOnly `key` names the storage namespace; the restored fields come from what is
|
|
587
|
-
* stored there.
|
|
588
|
-
*/
|
|
589
719
|
#resolveLogicalKey() {
|
|
590
720
|
const key = this.keyValue || this.element.id;
|
|
591
721
|
return key.length > 0 ? key : null;
|
|
592
722
|
}
|
|
593
|
-
/** Namespaces one logical key within localStorage. */
|
|
594
723
|
#storageKey(logicalKey) {
|
|
595
724
|
return `${STORAGE_PREFIX}${logicalKey}`;
|
|
596
725
|
}
|
|
597
|
-
/** Normalizes an invalid `debounce` Value to the `debounce` default. */
|
|
598
726
|
get #debounceDelay() {
|
|
599
|
-
|
|
600
|
-
return Number.isFinite(value) && value >= 0 ? value : DEFAULT_DEBOUNCE;
|
|
727
|
+
return this.#safeDebounce;
|
|
601
728
|
}
|
|
602
|
-
/** Dispatches one observable storage failure without exposing browser errors. */
|
|
603
729
|
#dispatchError(key, operation, reason) {
|
|
604
730
|
this.dispatch("error", { detail: { key, operation, reason } });
|
|
605
731
|
}
|
|
732
|
+
get #safeDebounce() {
|
|
733
|
+
return this.#numbers.read(
|
|
734
|
+
this,
|
|
735
|
+
"debounce",
|
|
736
|
+
this.debounceValue,
|
|
737
|
+
_PersistController.values.debounce.default,
|
|
738
|
+
_PersistController.valueConstraints.debounce
|
|
739
|
+
);
|
|
740
|
+
}
|
|
606
741
|
};
|
|
607
742
|
|
|
608
743
|
export { PersistController };
|
|
609
|
-
//# sourceMappingURL=persist_controller.js.map
|
|
610
744
|
//# sourceMappingURL=persist_controller.js.map
|