stimeo-ui 0.16.0 → 0.18.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.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +136 -0
  3. data/README.md +27 -11
  4. data/dist/cable/index.js +384 -449
  5. data/dist/controllers/accordion_controller.js +174 -55
  6. data/dist/controllers/alert_dialog_controller.js +903 -235
  7. data/dist/controllers/announcer_controller.js +238 -205
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +414 -145
  10. data/dist/controllers/avatar_controller.js +185 -107
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +282 -58
  13. data/dist/controllers/calendar_controller.js +228 -260
  14. data/dist/controllers/carousel_controller.js +536 -436
  15. data/dist/controllers/character_counter_controller.js +493 -207
  16. data/dist/controllers/checkbox_controller.js +38 -74
  17. data/dist/controllers/clipboard_controller.js +261 -149
  18. data/dist/controllers/collapsible_controller.js +304 -120
  19. data/dist/controllers/color_picker_controller.js +178 -146
  20. data/dist/controllers/combobox_controller.js +522 -156
  21. data/dist/controllers/command_palette_controller.js +1156 -395
  22. data/dist/controllers/conditional_fields_controller.js +300 -171
  23. data/dist/controllers/confirm_controller.js +980 -262
  24. data/dist/controllers/context_menu_controller.js +346 -153
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +281 -158
  27. data/dist/controllers/currency_input_controller.js +575 -225
  28. data/dist/controllers/data_grid_controller.js +248 -214
  29. data/dist/controllers/date_range_picker_controller.js +338 -235
  30. data/dist/controllers/dialog_controller.js +899 -237
  31. data/dist/controllers/direct_upload_controller.js +97 -167
  32. data/dist/controllers/dirty_form_controller.js +72 -69
  33. data/dist/controllers/dismissible_controller.js +196 -24
  34. data/dist/controllers/drawer_controller.js +974 -355
  35. data/dist/controllers/dropdown_controller.js +339 -96
  36. data/dist/controllers/editable_controller.js +315 -88
  37. data/dist/controllers/empty_state_controller.js +79 -58
  38. data/dist/controllers/file_dropzone_controller.js +322 -211
  39. data/dist/controllers/filter_controller.js +77 -42
  40. data/dist/controllers/flash_controller.js +238 -347
  41. data/dist/controllers/focus_controller.js +679 -240
  42. data/dist/controllers/form_field_controller.js +156 -132
  43. data/dist/controllers/form_validation_controller.js +31 -97
  44. data/dist/controllers/frame_loading_controller.js +275 -217
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +327 -130
  47. data/dist/controllers/idle_controller.js +271 -101
  48. data/dist/controllers/input_mask_controller.js +483 -97
  49. data/dist/controllers/intersection_controller.js +150 -106
  50. data/dist/controllers/lazy_frame_controller.js +24 -68
  51. data/dist/controllers/listbox_controller.js +523 -240
  52. data/dist/controllers/local_time_controller.js +102 -77
  53. data/dist/controllers/masonry_controller.js +80 -96
  54. data/dist/controllers/menu_controller.js +302 -188
  55. data/dist/controllers/menubar_controller.js +232 -345
  56. data/dist/controllers/meter_controller.js +417 -87
  57. data/dist/controllers/multi_select_controller.js +785 -393
  58. data/dist/controllers/navigation_menu_controller.js +190 -250
  59. data/dist/controllers/nested_form_controller.js +396 -192
  60. data/dist/controllers/network_status_controller.js +268 -62
  61. data/dist/controllers/number_input_controller.js +610 -303
  62. data/dist/controllers/optimistic_controller.js +45 -87
  63. data/dist/controllers/otp_controller.js +658 -255
  64. data/dist/controllers/overflow_indicator_controller.js +122 -113
  65. data/dist/controllers/overflow_menu_controller.js +470 -295
  66. data/dist/controllers/pagination_controller.js +332 -123
  67. data/dist/controllers/password_reveal_controller.js +392 -121
  68. data/dist/controllers/password_strength_controller.js +111 -158
  69. data/dist/controllers/persist_controller.js +165 -111
  70. data/dist/controllers/pointer_drag_controller.js +434 -188
  71. data/dist/controllers/popover_controller.js +269 -113
  72. data/dist/controllers/portal_controller.js +215 -76
  73. data/dist/controllers/preview_guard_controller.js +243 -116
  74. data/dist/controllers/progress_controller.js +391 -78
  75. data/dist/controllers/radio_group_controller.js +278 -211
  76. data/dist/controllers/range_slider_controller.js +220 -113
  77. data/dist/controllers/rating_controller.js +458 -211
  78. data/dist/controllers/read_more_controller.js +138 -63
  79. data/dist/controllers/reading_progress_controller.js +166 -137
  80. data/dist/controllers/relative_time_controller.js +211 -112
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  82. data/dist/controllers/resizable_controller.js +299 -150
  83. data/dist/controllers/roving_controller.js +49 -88
  84. data/dist/controllers/scroll_area_controller.js +490 -246
  85. data/dist/controllers/scroll_restore_controller.js +14 -55
  86. data/dist/controllers/scroll_visibility_controller.js +432 -149
  87. data/dist/controllers/scrollspy_controller.js +193 -248
  88. data/dist/controllers/separator_controller.js +285 -142
  89. data/dist/controllers/sidebar_controller.js +1069 -404
  90. data/dist/controllers/skeleton_controller.js +259 -101
  91. data/dist/controllers/slider_controller.js +187 -86
  92. data/dist/controllers/smart_sticky_header_controller.js +173 -66
  93. data/dist/controllers/sortable_controller.js +26 -125
  94. data/dist/controllers/spinner_controller.js +288 -173
  95. data/dist/controllers/step_indicator_controller.js +105 -46
  96. data/dist/controllers/stepper_controller.js +120 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +275 -140
  98. data/dist/controllers/sticky_observer_controller.js +220 -49
  99. data/dist/controllers/submit_once_controller.js +376 -227
  100. data/dist/controllers/switch_controller.js +201 -84
  101. data/dist/controllers/tabs_controller.js +41 -30
  102. data/dist/controllers/tags_input_controller.js +293 -173
  103. data/dist/controllers/textarea_autosize_controller.js +108 -74
  104. data/dist/controllers/theme_controller.js +140 -116
  105. data/dist/controllers/time_picker_controller.js +331 -127
  106. data/dist/controllers/toast_controller.js +244 -324
  107. data/dist/controllers/toggle_group_controller.js +252 -191
  108. data/dist/controllers/toolbar_controller.js +219 -120
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +283 -369
  112. data/dist/index.js +8573 -10410
  113. data/dist/positioning/index.js +154 -47
  114. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  115. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  116. data/lib/stimeo/ui/version.rb +1 -1
  117. metadata +3 -3
@@ -1,28 +1,95 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/persist_controller.ts
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 registry2 = create();
9
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
10
+ return registry2;
11
+ }
12
+
13
+ sharedRegistry(
14
+ "stimeo-ui.attribute-lease.registry.v1",
15
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
16
+ );
17
+ function isAttributeValue(value) {
18
+ return value === null || typeof value === "string";
19
+ }
20
+ function readLeaseRecord(element, record) {
21
+ try {
22
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
23
+ if (!Array.isArray(parsed)) return void 0;
24
+ const [original, written, ...rest] = parsed;
25
+ const linked = rest[0] !== false;
26
+ const beneath = linked ? rest : rest.slice(1);
27
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
28
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
29
+ } catch {
30
+ return void 0;
31
+ }
32
+ }
33
+ function leasedAuthorValue(element, attribute) {
34
+ const suffix = `-${attribute}-lease`;
35
+ for (const name of element.getAttributeNames()) {
36
+ if (!name.startsWith("data-") || !name.endsWith(suffix)) continue;
37
+ const record = readLeaseRecord(element, name);
38
+ if (record) return record.original;
39
+ }
40
+ return void 0;
41
+ }
42
+
43
+ var PREFIX = "__stimeo_raw_v1_";
44
+ sharedRegistry(
45
+ "stimeo-ui.form-data-projection.registry.v1",
46
+ () => /* @__PURE__ */ new WeakMap()
47
+ );
48
+ function decodedName(key) {
49
+ if (!key.startsWith(PREFIX)) return null;
50
+ const separator = PREFIX.length + 32;
51
+ if (key[separator] !== ":") return null;
52
+ try {
53
+ const name = JSON.parse(key.slice(separator + 1));
54
+ return typeof name === "string" ? name : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ function projectedFieldName(name) {
60
+ return decodedName(name) ?? name;
61
+ }
62
+
63
+ var ListenerSet = class {
64
+ #abort = new AbortController();
65
+ add(target, type, handler, options) {
66
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
67
+ }
68
+ dispose() {
69
+ this.#abort.abort();
70
+ this.#abort = new AbortController();
71
+ }
72
+ };
4
73
 
5
- // src/utils/microtask_coalescer.ts
6
74
  var MicrotaskCoalescer = class {
7
75
  #run;
8
76
  #queued = false;
9
77
  #active = false;
10
78
  #generation = 0;
11
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
12
79
  constructor(run) {
13
80
  this.#run = run;
14
81
  }
15
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
16
82
  activate() {
17
83
  this.#active = true;
18
84
  }
19
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
20
85
  cancel() {
21
86
  this.#active = false;
22
87
  this.#queued = false;
23
88
  this.#generation += 1;
24
89
  }
25
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
90
+ get pending() {
91
+ return this.#queued;
92
+ }
26
93
  schedule() {
27
94
  if (!this.#active || this.#queued) return;
28
95
  this.#queued = true;
@@ -35,7 +102,52 @@ var MicrotaskCoalescer = class {
35
102
  }
36
103
  };
37
104
 
38
- // src/utils/safe_storage.ts
105
+ var MAX_TIMER_DELAY_MS = 2147483647;
106
+
107
+ var NUMBER_BOUNDS = {
108
+ timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
109
+ function matchesNumberBounds(value, bounds) {
110
+ if (!Number.isFinite(value)) {
111
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
112
+ if (direction === null) return false;
113
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
114
+ }
115
+ if (bounds.min !== void 0 && value < bounds.min) return false;
116
+ if (bounds.max !== void 0 && value > bounds.max) return false;
117
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
118
+ if (bounds.integer && !Number.isInteger(value)) return false;
119
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
120
+ return true;
121
+ }
122
+
123
+ function readNumber(raw, fallback, bounds) {
124
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
125
+ }
126
+
127
+ var NumberValueReader = class {
128
+ #lastRejected = /* @__PURE__ */ new Map();
129
+ read(owner, name, raw, fallback, bounds) {
130
+ const resolved = readNumber(raw, fallback, bounds);
131
+ if (matchesNumberBounds(raw, bounds)) {
132
+ this.#lastRejected.delete(name);
133
+ return resolved;
134
+ }
135
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
136
+ const literal = owner.element.getAttribute(attribute);
137
+ if (literal === null) {
138
+ this.#lastRejected.delete(name);
139
+ return resolved;
140
+ }
141
+ if (this.#lastRejected.get(name) !== literal) {
142
+ this.#lastRejected.set(name, literal);
143
+ console.warn(
144
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
145
+ );
146
+ }
147
+ return resolved;
148
+ }
149
+ };
150
+
39
151
  function readLocalStorage(key) {
40
152
  try {
41
153
  return { ok: true, value: window.localStorage.getItem(key) };
@@ -60,43 +172,24 @@ function removeLocalStorage(key) {
60
172
  }
61
173
  }
62
174
 
63
- // src/utils/safe_timeout.ts
64
175
  var TimerRegistry = class {
65
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
66
176
  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
177
  clear(id) {
74
178
  if (this.ids.delete(id)) {
75
179
  this.cancel(id);
76
180
  }
77
181
  }
78
- /**
79
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
80
- * to guarantee no timer outlives the element.
81
- */
82
182
  clearAll() {
83
183
  for (const id of this.ids) {
84
184
  this.cancel(id);
85
185
  }
86
186
  this.ids.clear();
87
187
  }
88
- /** Number of timers currently tracked (pending). */
89
188
  get size() {
90
189
  return this.ids.size;
91
190
  }
92
191
  };
93
192
  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
193
  set(callback, delay) {
101
194
  const id = this.schedule(() => {
102
195
  this.ids.delete(id);
@@ -113,7 +206,6 @@ var SafeTimeout = class extends TimerRegistry {
113
206
  }
114
207
  };
115
208
 
116
- // src/utils/string_list.ts
117
209
  function parseStringList(raw, fallback = []) {
118
210
  const text = raw.trim();
119
211
  if (text.length === 0) return [...fallback];
@@ -127,19 +219,16 @@ function parseStringList(raw, fallback = []) {
127
219
  return parsed.filter((entry) => typeof entry === "string");
128
220
  }
129
221
 
130
- // src/utils/transient_hooks.ts
131
222
  var TransientHooks = class {
132
223
  #attributes;
133
224
  constructor(options) {
134
225
  this.#attributes = options.attributes;
135
226
  }
136
- /** Drops the declared hooks from `element`; what it carries besides stays. */
137
227
  reset(element) {
138
228
  for (const attribute of this.#attributes) element.removeAttribute(attribute);
139
229
  }
140
230
  };
141
231
 
142
- // src/controllers/persist_controller.ts
143
232
  var TRANSIENT = new TransientHooks({ attributes: ["data-persist-restored"] });
144
233
  var NON_VALUE_TYPES = /* @__PURE__ */ new Set(["file", "submit", "reset", "button", "image"]);
145
234
  var SENSITIVE_TYPES = /* @__PURE__ */ new Set(["password"]);
@@ -171,15 +260,18 @@ var parsePayload = (raw) => {
171
260
  return { version: 1, fields };
172
261
  };
173
262
  var isPersistField = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement;
174
- var PersistController = class extends Controller {
263
+ var PersistController = class _PersistController extends Controller {
264
+ #numbers = new NumberValueReader();
175
265
  static targets = ["field"];
176
266
  static values = {
177
267
  key: { type: String, default: "" },
178
268
  debounce: { type: Number, default: DEFAULT_DEBOUNCE },
179
- // A malformed JSON list must fall back without aborting Stimulus connection.
180
269
  exclude: { type: String, default: "" },
181
270
  clearOn: { type: String, default: "" }
182
271
  };
272
+ static valueConstraints = {
273
+ debounce: NUMBER_BOUNDS.timer
274
+ };
183
275
  static actions = ["clear"];
184
276
  static events = ["restore", "save", "clear", "error"];
185
277
  #timeouts = new SafeTimeout();
@@ -190,6 +282,7 @@ var PersistController = class extends Controller {
190
282
  #logicalKey = null;
191
283
  #payload = null;
192
284
  #registeredClearOn = null;
285
+ #clearOnListeners = new ListenerSet();
193
286
  #excluded = DEFAULT_EXCLUDE;
194
287
  #knownFields = /* @__PURE__ */ new WeakSet();
195
288
  #forcedRestore = /* @__PURE__ */ new Set();
@@ -203,19 +296,19 @@ var PersistController = class extends Controller {
203
296
  #onClearEvent = () => {
204
297
  this.clear();
205
298
  };
299
+ #listeners = new ListenerSet();
206
300
  connect() {
207
301
  if (this.#connected) return;
208
302
  this.#connected = true;
209
303
  this.#logicalKey = this.#resolveLogicalKey();
210
304
  this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);
211
- this.#knownFields = /* @__PURE__ */ new WeakSet();
212
- this.#forcedRestore.clear();
213
- this.element.addEventListener("input", this.#onInput);
214
- this.element.addEventListener("change", this.#onInput);
305
+ this.#listeners.add(this.element, "input", this.#onInput);
306
+ this.#listeners.add(this.element, "change", this.#onInput);
215
307
  this.#syncClearOn();
216
308
  this.#restoreDynamic.activate();
217
309
  this.#observer.observe(this.element, {
218
310
  attributes: true,
311
+ attributeOldValue: true,
219
312
  attributeFilter: [
220
313
  "checked",
221
314
  "data-controller",
@@ -235,17 +328,13 @@ var PersistController = class extends Controller {
235
328
  disconnect() {
236
329
  this.#flushPendingSave();
237
330
  this.#connected = false;
238
- this.element.removeEventListener("input", this.#onInput);
239
- this.element.removeEventListener("change", this.#onInput);
331
+ this.#listeners.dispose();
240
332
  this.#unbindClearOn();
241
333
  this.#observer.disconnect();
242
334
  this.#restoreDynamic.cancel();
243
- this.#forcedRestore.clear();
244
- this.#timeouts.clearAll();
245
335
  TRANSIENT.reset(this.element);
246
336
  this.#payload = null;
247
337
  }
248
- /** Drops the active draft after storage confirms the removal. */
249
338
  clear() {
250
339
  const logicalKey = this.#logicalKey ?? this.#resolveLogicalKey();
251
340
  if (logicalKey === null) return;
@@ -259,19 +348,12 @@ var PersistController = class extends Controller {
259
348
  this.element.removeAttribute("data-persist-restored");
260
349
  this.dispatch("clear", { detail: { key: logicalKey } });
261
350
  }
262
- /** Rebinds the external clear event when its Value changes. */
263
351
  clearOnValueChanged() {
264
352
  if (this.#connected) this.#syncClearOn();
265
353
  }
266
- /** Switches storage namespaces without writing old edits under the new key. */
267
354
  keyValueChanged() {
268
355
  if (this.#connected) this.#switchLogicalKey();
269
356
  }
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
357
  excludeValueChanged() {
276
358
  const previous = this.#excluded;
277
359
  this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);
@@ -283,7 +365,6 @@ var PersistController = class extends Controller {
283
365
  }
284
366
  this.#restoreDynamic.schedule();
285
367
  }
286
- /** Schedules a debounced save. */
287
368
  #scheduleSave() {
288
369
  const logicalKey = this.#logicalKey;
289
370
  if (logicalKey === null) return;
@@ -294,21 +375,19 @@ var PersistController = class extends Controller {
294
375
  }, this.#debounceDelay);
295
376
  this.#pendingSave = { id, logicalKey };
296
377
  }
297
- /** Cancels the currently pending save, if any. */
298
378
  #cancelPendingSave() {
299
379
  const pending = this.#pendingSave;
300
380
  if (pending === null) return;
301
381
  this.#timeouts.clear(pending.id);
302
382
  this.#pendingSave = null;
303
383
  }
304
- /** Flushes a pending edit through the same success/error event path as a timer. */
305
384
  #flushPendingSave() {
306
385
  const pending = this.#pendingSave;
307
386
  if (pending === null) return;
308
387
  this.#cancelPendingSave();
309
388
  this.#save(pending.logicalKey);
389
+ this.#cancelPendingSave();
310
390
  }
311
- /** Writes the active payload and emits `save` only after storage succeeds. */
312
391
  #save(logicalKey) {
313
392
  const payload = this.#serialize();
314
393
  const result = writeLocalStorage(this.#storageKey(logicalKey), JSON.stringify(payload));
@@ -320,7 +399,6 @@ var PersistController = class extends Controller {
320
399
  this.dispatch("save", { detail: { key: logicalKey } });
321
400
  return true;
322
401
  }
323
- /** Serializes every persistable field into a versioned, typed payload. */
324
402
  #serialize() {
325
403
  const fields = [];
326
404
  for (const { field, key } of this.#fieldEntries()) {
@@ -339,17 +417,13 @@ var PersistController = class extends Controller {
339
417
  }
340
418
  return { version: 1, fields };
341
419
  }
342
- /** Reads, validates, and restores the active namespace. */
343
420
  #loadActiveDraft() {
344
421
  TRANSIENT.reset(this.element);
345
422
  this.#payload = null;
346
423
  this.#knownFields = /* @__PURE__ */ new WeakSet();
347
424
  this.#forcedRestore.clear();
348
425
  const logicalKey = this.#logicalKey;
349
- if (logicalKey === null) {
350
- this.#markCurrentFieldsKnown();
351
- return;
352
- }
426
+ if (logicalKey === null) return;
353
427
  const result = readLocalStorage(this.#storageKey(logicalKey));
354
428
  if (!result.ok) {
355
429
  this.#dispatchError(logicalKey, "read", "unavailable");
@@ -371,7 +445,6 @@ var PersistController = class extends Controller {
371
445
  this.#payload = payload;
372
446
  this.#restoreEntries(null);
373
447
  }
374
- /** Restores fields inserted or made eligible after the initial connection. */
375
448
  #restoreDynamicFields() {
376
449
  const selected = new Set(this.#forcedRestore);
377
450
  this.#forcedRestore.clear();
@@ -380,7 +453,6 @@ var PersistController = class extends Controller {
380
453
  }
381
454
  this.#restoreEntries(selected);
382
455
  }
383
- /** Applies the active payload to all fields, or only the supplied candidates. */
384
456
  #restoreEntries(selected) {
385
457
  const entries = this.#fieldEntries();
386
458
  for (const { field } of entries) this.#knownFields.add(field);
@@ -413,7 +485,6 @@ var PersistController = class extends Controller {
413
485
  this.dispatch("restore", { detail: { key: this.#logicalKey } });
414
486
  }
415
487
  }
416
- /** Applies a type-compatible stored value without coercion. */
417
488
  #applyValue(field, value) {
418
489
  if (field instanceof HTMLInputElement && field.type === "checkbox") {
419
490
  if (typeof value !== "boolean") return false;
@@ -439,16 +510,11 @@ var PersistController = class extends Controller {
439
510
  field.value = value;
440
511
  return true;
441
512
  }
442
- /** Every control this instance owns, before the exclusion list narrows them. */
443
513
  #candidateFields() {
444
514
  return this.hasFieldTarget ? this.fieldTargets.filter(isPersistField) : Array.from(this.element.querySelectorAll("input, textarea, select")).filter(
445
515
  (field) => this.#ownsField(field)
446
516
  );
447
517
  }
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
518
  #fieldEntries() {
453
519
  const entries = [];
454
520
  const eligible = [];
@@ -480,26 +546,23 @@ var PersistController = class extends Controller {
480
546
  }
481
547
  return entries;
482
548
  }
483
- /** Whether a field carries a restorable, non-excluded value. */
484
549
  #persistable(field, excluded) {
485
550
  if (NON_VALUE_TYPES.has(field.type) || SENSITIVE_TYPES.has(field.type)) return false;
551
+ if (SENSITIVE_TYPES.has(leasedAuthorValue(field, "type") ?? "")) return false;
486
552
  if (excluded.includes(field.type)) return false;
487
- if (field.name.length > 0 && excluded.includes(field.name)) return false;
553
+ const name = projectedFieldName(field.name);
554
+ if (name.length > 0 && excluded.includes(name)) return false;
488
555
  return true;
489
556
  }
490
- /** A stable storage sub-key for a field (its name, else id). */
491
557
  #keyOf(field) {
492
- return field.name || field.id || null;
558
+ return projectedFieldName(field.name) || field.id || null;
493
559
  }
494
- /** Whether this instance, rather than a nested Persist host, owns a field. */
495
560
  #ownsField(field) {
496
561
  return field.closest(`[data-controller~="${this.identifier}"]`) === this.element;
497
562
  }
498
- /** Marks all currently eligible fields without applying a payload. */
499
563
  #markCurrentFieldsKnown() {
500
564
  for (const { field } of this.#fieldEntries()) this.#knownFields.add(field);
501
565
  }
502
- /** Switches namespace after flushing the old namespace's pending edit. */
503
566
  #switchLogicalKey() {
504
567
  const next = this.#resolveLogicalKey();
505
568
  if (next === this.#logicalKey) return;
@@ -507,61 +570,55 @@ var PersistController = class extends Controller {
507
570
  this.#logicalKey = next;
508
571
  this.#loadActiveDraft();
509
572
  }
510
- /** Synchronizes the exact event name owned by the `clearOn` Value. */
511
573
  #syncClearOn() {
512
574
  const next = this.#validEventName(this.clearOnValue);
513
575
  if (next === this.#registeredClearOn) return;
514
576
  this.#unbindClearOn();
515
577
  if (next === null) return;
516
- this.element.addEventListener(next, this.#onClearEvent);
578
+ this.#clearOnListeners.add(this.element, next, this.#onClearEvent);
517
579
  this.#registeredClearOn = next;
518
580
  }
519
- /** Removes the event listener using the name that was actually registered. */
520
581
  #unbindClearOn() {
521
582
  if (this.#registeredClearOn === null) return;
522
- this.element.removeEventListener(this.#registeredClearOn, this.#onClearEvent);
583
+ this.#clearOnListeners.dispose();
523
584
  this.#registeredClearOn = null;
524
585
  }
525
- /** Returns a non-whitespace event type, or null when the hook is disabled. */
526
586
  #validEventName(value) {
527
587
  const name = value.trim();
528
588
  return name.length > 0 && !/\s/.test(name) ? name : null;
529
589
  }
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
590
  #onMutations(records) {
537
591
  let rootIdChanged = false;
592
+ const ownershipBefore = /* @__PURE__ */ new Map();
538
593
  for (const record of records) {
539
594
  if (record.type === "attributes") {
540
595
  if (record.target === this.element && record.attributeName === "id") {
541
596
  rootIdChanged = true;
542
597
  }
543
- this.#collectAttributeCandidate(record.target, record.attributeName);
598
+ if (record.attributeName === OWNERSHIP_ATTRIBUTE) {
599
+ const element = record.target;
600
+ if (!ownershipBefore.has(element)) ownershipBefore.set(element, record.oldValue);
601
+ continue;
602
+ }
603
+ this.#collectControlCandidate(record.target);
544
604
  continue;
545
605
  }
546
606
  for (const node of record.addedNodes) this.#collectRestoreCandidate(node);
547
607
  }
608
+ for (const [element, before] of ownershipBefore) {
609
+ this.#collectOwnershipCandidate(element, before);
610
+ }
548
611
  if (rootIdChanged && this.keyValue.length === 0) this.#switchLogicalKey();
549
612
  this.#restoreDynamic.schedule();
550
613
  }
551
- /**
552
- * Adds the controls one mutated attribute can affect. Only an ownership change on a
553
- * descendant moves fields between Persist hosts, so that is the single case worth a
554
- * subtree sweep; every other observed attribute describes one control, and sweeping
555
- * from its container would re-apply the stored draft over edits still being debounced.
556
- */
557
- #collectAttributeCandidate(target, attributeName) {
558
- if (attributeName === OWNERSHIP_ATTRIBUTE && target !== this.element) {
559
- this.#collectRestoreCandidate(target);
560
- return;
561
- }
562
- this.#collectControlCandidate(target);
614
+ #collectOwnershipCandidate(element, before) {
615
+ const after = element.getAttribute(OWNERSHIP_ATTRIBUTE);
616
+ if (this.#namesThisIdentifier(before) === this.#namesThisIdentifier(after)) return;
617
+ this.#collectRestoreCandidate(element);
618
+ }
619
+ #namesThisIdentifier(tokens) {
620
+ return (tokens ?? "").split(/\s+/).includes(this.identifier);
563
621
  }
564
- /** Adds a native control, or the select that owns a mutated option. */
565
622
  #collectControlCandidate(node) {
566
623
  if (isPersistField(node)) {
567
624
  this.#forcedRestore.add(node);
@@ -572,7 +629,6 @@ var PersistController = class extends Controller {
572
629
  if (select) this.#forcedRestore.add(select);
573
630
  }
574
631
  }
575
- /** Adds every control inside an inserted or newly owned subtree. */
576
632
  #collectRestoreCandidate(node) {
577
633
  this.#collectControlCandidate(node);
578
634
  if (!(node instanceof Element)) return;
@@ -580,31 +636,29 @@ var PersistController = class extends Controller {
580
636
  this.#forcedRestore.add(field);
581
637
  }
582
638
  }
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
639
  #resolveLogicalKey() {
590
640
  const key = this.keyValue || this.element.id;
591
641
  return key.length > 0 ? key : null;
592
642
  }
593
- /** Namespaces one logical key within localStorage. */
594
643
  #storageKey(logicalKey) {
595
644
  return `${STORAGE_PREFIX}${logicalKey}`;
596
645
  }
597
- /** Normalizes an invalid `debounce` Value to the `debounce` default. */
598
646
  get #debounceDelay() {
599
- const value = this.debounceValue;
600
- return Number.isFinite(value) && value >= 0 ? value : DEFAULT_DEBOUNCE;
647
+ return this.#safeDebounce;
601
648
  }
602
- /** Dispatches one observable storage failure without exposing browser errors. */
603
649
  #dispatchError(key, operation, reason) {
604
650
  this.dispatch("error", { detail: { key, operation, reason } });
605
651
  }
652
+ get #safeDebounce() {
653
+ return this.#numbers.read(
654
+ this,
655
+ "debounce",
656
+ this.debounceValue,
657
+ _PersistController.values.debounce.default,
658
+ _PersistController.valueConstraints.debounce
659
+ );
660
+ }
606
661
  };
607
662
 
608
663
  export { PersistController };
609
- //# sourceMappingURL=persist_controller.js.map
610
664
  //# sourceMappingURL=persist_controller.js.map