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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +8 -1
  4. data/dist/cable/index.js +363 -436
  5. data/dist/controllers/accordion_controller.js +166 -55
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -198
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +271 -127
  10. data/dist/controllers/avatar_controller.js +171 -105
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +262 -52
  13. data/dist/controllers/calendar_controller.js +175 -209
  14. data/dist/controllers/carousel_controller.js +461 -399
  15. data/dist/controllers/character_counter_controller.js +246 -156
  16. data/dist/controllers/checkbox_controller.js +32 -74
  17. data/dist/controllers/clipboard_controller.js +168 -147
  18. data/dist/controllers/collapsible_controller.js +301 -120
  19. data/dist/controllers/color_picker_controller.js +149 -139
  20. data/dist/controllers/combobox_controller.js +360 -133
  21. data/dist/controllers/command_palette_controller.js +970 -358
  22. data/dist/controllers/conditional_fields_controller.js +166 -124
  23. data/dist/controllers/confirm_controller.js +939 -248
  24. data/dist/controllers/context_menu_controller.js +256 -128
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +183 -148
  27. data/dist/controllers/currency_input_controller.js +134 -158
  28. data/dist/controllers/data_grid_controller.js +114 -186
  29. data/dist/controllers/date_range_picker_controller.js +288 -206
  30. data/dist/controllers/dialog_controller.js +858 -223
  31. data/dist/controllers/direct_upload_controller.js +73 -153
  32. data/dist/controllers/dirty_form_controller.js +14 -48
  33. data/dist/controllers/dismissible_controller.js +175 -21
  34. data/dist/controllers/drawer_controller.js +933 -341
  35. data/dist/controllers/dropdown_controller.js +312 -82
  36. data/dist/controllers/editable_controller.js +158 -68
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +311 -207
  39. data/dist/controllers/filter_controller.js +67 -39
  40. data/dist/controllers/flash_controller.js +172 -314
  41. data/dist/controllers/focus_controller.js +654 -240
  42. data/dist/controllers/form_field_controller.js +147 -132
  43. data/dist/controllers/form_validation_controller.js +13 -86
  44. data/dist/controllers/frame_loading_controller.js +264 -213
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +309 -118
  47. data/dist/controllers/idle_controller.js +267 -101
  48. data/dist/controllers/input_mask_controller.js +71 -72
  49. data/dist/controllers/intersection_controller.js +147 -106
  50. data/dist/controllers/lazy_frame_controller.js +9 -65
  51. data/dist/controllers/listbox_controller.js +306 -204
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -93
  54. data/dist/controllers/menu_controller.js +214 -156
  55. data/dist/controllers/menubar_controller.js +170 -329
  56. data/dist/controllers/meter_controller.js +151 -67
  57. data/dist/controllers/multi_select_controller.js +593 -358
  58. data/dist/controllers/navigation_menu_controller.js +124 -232
  59. data/dist/controllers/nested_form_controller.js +352 -182
  60. data/dist/controllers/network_status_controller.js +242 -54
  61. data/dist/controllers/number_input_controller.js +320 -263
  62. data/dist/controllers/optimistic_controller.js +16 -76
  63. data/dist/controllers/otp_controller.js +198 -193
  64. data/dist/controllers/overflow_indicator_controller.js +116 -113
  65. data/dist/controllers/overflow_menu_controller.js +343 -283
  66. data/dist/controllers/pagination_controller.js +295 -119
  67. data/dist/controllers/password_reveal_controller.js +389 -121
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +237 -103
  70. data/dist/controllers/pointer_drag_controller.js +185 -165
  71. data/dist/controllers/popover_controller.js +236 -97
  72. data/dist/controllers/portal_controller.js +209 -76
  73. data/dist/controllers/preview_guard_controller.js +232 -116
  74. data/dist/controllers/progress_controller.js +125 -56
  75. data/dist/controllers/radio_group_controller.js +117 -137
  76. data/dist/controllers/range_slider_controller.js +192 -105
  77. data/dist/controllers/rating_controller.js +418 -207
  78. data/dist/controllers/read_more_controller.js +134 -63
  79. data/dist/controllers/reading_progress_controller.js +147 -135
  80. data/dist/controllers/relative_time_controller.js +145 -110
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  82. data/dist/controllers/resizable_controller.js +255 -139
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -225
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -145
  87. data/dist/controllers/scrollspy_controller.js +181 -242
  88. data/dist/controllers/separator_controller.js +267 -140
  89. data/dist/controllers/sidebar_controller.js +1023 -386
  90. data/dist/controllers/skeleton_controller.js +248 -97
  91. data/dist/controllers/slider_controller.js +159 -79
  92. data/dist/controllers/smart_sticky_header_controller.js +162 -63
  93. data/dist/controllers/sortable_controller.js +0 -112
  94. data/dist/controllers/spinner_controller.js +277 -169
  95. data/dist/controllers/step_indicator_controller.js +99 -46
  96. data/dist/controllers/stepper_controller.js +114 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +262 -138
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -205
  100. data/dist/controllers/switch_controller.js +33 -60
  101. data/dist/controllers/tabs_controller.js +35 -29
  102. data/dist/controllers/tags_input_controller.js +136 -145
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +130 -112
  105. data/dist/controllers/time_picker_controller.js +146 -80
  106. data/dist/controllers/toast_controller.js +186 -297
  107. data/dist/controllers/toggle_group_controller.js +95 -136
  108. data/dist/controllers/toolbar_controller.js +68 -98
  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 +150 -345
  112. data/dist/index.js +6370 -9346
  113. data/dist/positioning/index.js +148 -47
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,8 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/direct_upload_controller.ts
4
-
5
- // src/utils/announce.ts
6
3
  function announce(message, options = {}) {
7
4
  const text = message.trim();
8
5
  if (text.length === 0) return;
@@ -19,36 +16,6 @@ function fillTemplate(template, values) {
19
16
  });
20
17
  }
21
18
 
22
- // src/utils/before_cache_reset.ts
23
- var BeforeCacheReset = class _BeforeCacheReset {
24
- /** Every subscribed instance, iterated by the one shared document listener. */
25
- static #subscribers = /* @__PURE__ */ new Set();
26
- /** The shared listener; installed while at least one instance is subscribed. */
27
- static #onBeforeCache = () => {
28
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
29
- };
30
- #rewind;
31
- /** @param rewind - the pass that returns this controller's state to its initial form. */
32
- constructor(rewind) {
33
- this.#rewind = rewind;
34
- }
35
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
36
- activate() {
37
- const first = _BeforeCacheReset.#subscribers.size === 0;
38
- _BeforeCacheReset.#subscribers.add(this);
39
- if (first) {
40
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
41
- }
42
- }
43
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
44
- deactivate() {
45
- _BeforeCacheReset.#subscribers.delete(this);
46
- if (_BeforeCacheReset.#subscribers.size > 0) return;
47
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
48
- }
49
- };
50
-
51
- // src/utils/declared_value.ts
52
19
  function parseDeclared(raw, parse, fallback) {
53
20
  try {
54
21
  return parse(raw);
@@ -68,43 +35,24 @@ function validSelector(element, raw, fallback) {
68
35
  );
69
36
  }
70
37
 
71
- // src/utils/safe_timeout.ts
72
38
  var TimerRegistry = class {
73
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
74
39
  ids = /* @__PURE__ */ new Set();
75
- /**
76
- * Cancels a single tracked timer.
77
- *
78
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
79
- * registry), so callers can clear defensively without guarding.
80
- */
81
40
  clear(id) {
82
41
  if (this.ids.delete(id)) {
83
42
  this.cancel(id);
84
43
  }
85
44
  }
86
- /**
87
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
88
- * to guarantee no timer outlives the element.
89
- */
90
45
  clearAll() {
91
46
  for (const id of this.ids) {
92
47
  this.cancel(id);
93
48
  }
94
49
  this.ids.clear();
95
50
  }
96
- /** Number of timers currently tracked (pending). */
97
51
  get size() {
98
52
  return this.ids.size;
99
53
  }
100
54
  };
101
55
  var SafeTimeout = class extends TimerRegistry {
102
- /**
103
- * Schedules `callback` after `delay` ms and returns the timer id.
104
- *
105
- * The id is removed from the registry automatically when the timeout fires,
106
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
107
- */
108
56
  set(callback, delay) {
109
57
  const id = this.schedule(() => {
110
58
  this.ids.delete(id);
@@ -121,14 +69,37 @@ var SafeTimeout = class extends TimerRegistry {
121
69
  }
122
70
  };
123
71
 
124
- // src/utils/template_row.ts
72
+ var KeyedTimers = class {
73
+ #timers = new SafeTimeout();
74
+ #pending = /* @__PURE__ */ new Map();
75
+ set(key, callback, delay) {
76
+ this.clear(key);
77
+ const id = this.#timers.set(() => {
78
+ this.#pending.delete(key);
79
+ callback();
80
+ }, delay);
81
+ this.#pending.set(key, id);
82
+ }
83
+ clear(key) {
84
+ this.#timers.clear(this.#pending.get(key) ?? -1);
85
+ this.#pending.delete(key);
86
+ }
87
+ clearAll() {
88
+ this.#timers.clearAll();
89
+ this.#pending.clear();
90
+ }
91
+ has(key) {
92
+ return this.#pending.has(key);
93
+ }
94
+ };
95
+
125
96
  function cloneTemplateRoot(template) {
126
97
  const root = template.content.firstElementChild;
127
98
  return root instanceof HTMLElement ? root.cloneNode(true) : null;
128
99
  }
129
100
 
130
- // src/controllers/direct_upload_controller.ts
131
101
  var REMOVE_DELAY = 4e3;
102
+ var GENERATED_ATTRIBUTE = "generated";
132
103
  var DirectUploadController = class extends Controller {
133
104
  static targets = ["list", "row"];
134
105
  static values = {
@@ -138,10 +109,9 @@ var DirectUploadController = class extends Controller {
138
109
  scope: { type: String, default: "" }
139
110
  };
140
111
  static events = ["progress", "done", "error", "reconcile"];
141
- #timeouts = new SafeTimeout();
112
+ #removals = new KeyedTimers();
142
113
  #rows = /* @__PURE__ */ new Map();
143
- #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
144
- /** The validated `scope` selector; a broken declaration falls back to `""`. */
114
+ #connected = false;
145
115
  #scopeSelector = "";
146
116
  #onInitialize = (event) => {
147
117
  if (!this.#inScope(event)) return;
@@ -165,26 +135,47 @@ var DirectUploadController = class extends Controller {
165
135
  this.#complete(this.#key(detail.id), this.#name(detail));
166
136
  };
167
137
  connect() {
138
+ this.#connected = true;
168
139
  document.addEventListener("direct-upload:initialize", this.#onInitialize);
169
140
  document.addEventListener("direct-upload:progress", this.#onProgress);
170
141
  document.addEventListener("direct-upload:error", this.#onError);
171
142
  document.addEventListener("direct-upload:end", this.#onEnd);
172
- this.#beforeCache.activate();
173
- this.#rescheduleRemovals();
143
+ this.#discardInherited();
144
+ this.#syncRemovals();
174
145
  }
175
146
  disconnect() {
147
+ this.#connected = false;
176
148
  document.removeEventListener("direct-upload:initialize", this.#onInitialize);
177
149
  document.removeEventListener("direct-upload:progress", this.#onProgress);
178
150
  document.removeEventListener("direct-upload:error", this.#onError);
179
151
  document.removeEventListener("direct-upload:end", this.#onEnd);
180
- this.#beforeCache.deactivate();
181
- this.#timeouts.clearAll();
152
+ this.#removals.clearAll();
153
+ }
154
+ get #generatedAttribute() {
155
+ return `data-${this.identifier}-${GENERATED_ATTRIBUTE}`;
156
+ }
157
+ #discardInherited() {
158
+ if (this.#rows.size > 0 || !this.hasListTarget) return;
159
+ const ids = [];
160
+ for (const row of Array.from(this.listTarget.children)) {
161
+ const id = row.getAttribute(this.#generatedAttribute);
162
+ if (id === null) continue;
163
+ row.remove();
164
+ ids.push(id);
165
+ }
166
+ if (ids.length === 0) return;
167
+ this.#syncAggregate();
168
+ this.dispatch("reconcile", { detail: { ids } });
182
169
  }
183
- /** Validates `scope` once so the per-event path never parses or throws. */
184
170
  scopeValueChanged() {
185
171
  this.#scopeSelector = validSelector(this.element, this.scopeValue, "");
186
172
  }
187
- /** Updates a row's progress and the aggregate, emitting `progress`. */
173
+ removeOnDoneValueChanged() {
174
+ if (this.#connected) this.#syncRemovals();
175
+ }
176
+ listTargetDisconnected() {
177
+ if (this.#connected) this.#syncAggregate();
178
+ }
188
179
  #updateProgress(id, percent, name) {
189
180
  const row = this.#rowFor(id, name);
190
181
  if (row === null || this.#isSettled(row)) return;
@@ -192,12 +183,6 @@ var DirectUploadController = class extends Controller {
192
183
  this.#syncAggregate();
193
184
  this.dispatch("progress", { detail: { id, percent: clamped } });
194
185
  }
195
- /**
196
- * Marks a not-yet-settled row done at 100%, announces it, and emits `done`.
197
- *
198
- * @stimeoRuntimeOnly `announceDoneText` words the one announcement of this completion and
199
- * `removeOnDone` decides whether it arms the row's removal.
200
- */
201
186
  #complete(id, name) {
202
187
  const row = this.#rowFor(id, name);
203
188
  if (row === null || this.#isSettled(row)) return;
@@ -206,18 +191,8 @@ var DirectUploadController = class extends Controller {
206
191
  this.#syncAggregate();
207
192
  this.#announce(this.announceDoneTextValue, name, row);
208
193
  this.dispatch("done", { detail: { id } });
209
- if (this.removeOnDoneValue) {
210
- this.#timeouts.set(() => this.#removeRow(id), REMOVE_DELAY);
211
- }
194
+ if (this.removeOnDoneValue) this.#scheduleRemoval(id, row);
212
195
  }
213
- /**
214
- * Marks a not-yet-settled row failed, announces it, and emits `error`.
215
- * Returns whether the failure is rendered by this widget (used to decide the
216
- * `direct-upload:error` default), which also holds when the row already
217
- * displays an earlier failure.
218
- *
219
- * @stimeoRuntimeOnly `announceErrorText` words the one announcement of this failure.
220
- */
221
196
  #fail(id, error, name) {
222
197
  const row = this.#rowFor(id, name);
223
198
  if (row === null) return false;
@@ -227,27 +202,25 @@ var DirectUploadController = class extends Controller {
227
202
  this.dispatch("error", { detail: { id, error } });
228
203
  return true;
229
204
  }
230
- /** Whether the row reached a terminal state; settled rows are never rewritten. */
231
205
  #isSettled(row) {
232
206
  const state = row.getAttribute("data-upload-state");
233
207
  return state === "done" || state === "error";
234
208
  }
235
- /**
236
- * Re-arms `removeOnDone` for completed rows after a reconnect.
237
- *
238
- * @stimeoRuntimeOnly `removeOnDone` decides whether reconnecting arms the removal of finished
239
- * rows again.
240
- */
241
- #rescheduleRemovals() {
242
- if (!this.removeOnDoneValue) return;
209
+ #syncRemovals() {
210
+ if (!this.removeOnDoneValue) {
211
+ this.#removals.clearAll();
212
+ return;
213
+ }
243
214
  this.#prune();
244
215
  for (const [id, row] of this.#rows) {
245
- if (row.getAttribute("data-upload-state") === "done") {
246
- this.#timeouts.set(() => this.#removeRow(id), REMOVE_DELAY);
216
+ if (row.getAttribute("data-upload-state") === "done" && !this.#removals.has(id)) {
217
+ this.#scheduleRemoval(id, row);
247
218
  }
248
219
  }
249
220
  }
250
- /** Returns the live row for `id`, creating (and labeling) one on first sight. */
221
+ #scheduleRemoval(id, row) {
222
+ this.#removals.set(id, () => this.#removeRow(row), REMOVE_DELAY);
223
+ }
251
224
  #rowFor(id, name) {
252
225
  const key = this.#key(id);
253
226
  const existing = this.#rows.get(key);
@@ -256,13 +229,13 @@ var DirectUploadController = class extends Controller {
256
229
  this.#applyName(existing, name);
257
230
  return existing;
258
231
  }
259
- existing.remove();
260
- this.#rows.delete(key);
232
+ this.#retire(key, existing);
261
233
  }
262
234
  if (!this.hasRowTarget || !this.hasListTarget) return null;
263
235
  const clone = cloneTemplateRoot(this.rowTarget);
264
236
  if (!clone) return null;
265
237
  this.#applyName(clone, name);
238
+ clone.setAttribute(this.#generatedAttribute, key);
266
239
  clone.setAttribute("data-upload-state", "uploading");
267
240
  this.#applyProgress(clone, 0);
268
241
  this.listTarget.appendChild(clone);
@@ -270,20 +243,12 @@ var DirectUploadController = class extends Controller {
270
243
  this.#syncAggregate();
271
244
  return clone;
272
245
  }
273
- /**
274
- * Writes the event's file name into `[data-field="name"]` and, unless the row
275
- * already carries a non-blank `aria-label` — authored on the template or
276
- * applied by an earlier event — makes it the accessible name too. The visible
277
- * name never depends on the label: an authored label keeps its wording while
278
- * the field still shows which file this row tracks.
279
- */
280
246
  #applyName(row, name) {
281
247
  if (name.length === 0) return;
282
248
  this.#setField(row, "name", name);
283
249
  if ((row.getAttribute("aria-label") ?? "").trim().length > 0) return;
284
250
  row.setAttribute("aria-label", name);
285
251
  }
286
- /** Writes one progress value to every per-row hook; returns the clamped percent. */
287
252
  #applyProgress(row, percent) {
288
253
  const clamped = Math.max(0, Math.min(100, Math.round(percent)));
289
254
  row.setAttribute("aria-valuenow", String(clamped));
@@ -292,17 +257,10 @@ var DirectUploadController = class extends Controller {
292
257
  this.#setField(row, "percent", `${clamped}%`);
293
258
  return clamped;
294
259
  }
295
- #removeRow(id) {
296
- const row = this.#rows.get(id);
297
- if (row === void 0) return;
260
+ #removeRow(row) {
298
261
  row.remove();
299
- this.#rows.delete(id);
300
262
  this.#syncAggregate();
301
263
  }
302
- /**
303
- * Reflects the average progress across live rows on the controller element,
304
- * withdrawing both hooks once no rows remain.
305
- */
306
264
  #syncAggregate() {
307
265
  this.#prune();
308
266
  if (this.#rows.size === 0) {
@@ -318,50 +276,21 @@ var DirectUploadController = class extends Controller {
318
276
  this.element.setAttribute("data-upload-progress", String(overall));
319
277
  this.element.style.setProperty("--stimeo--upload-progress", `${overall}%`);
320
278
  }
321
- /**
322
- * Whether a bookkept row is still this widget's live UI: connected, and — when
323
- * a `list` target is present — inside the *current* one, so re-pointing the
324
- * target attribute at a new element retires rows kept alive in the old list.
325
- */
326
279
  #tracksRow(row) {
327
280
  if (!row.isConnected) return false;
328
281
  return !this.hasListTarget || this.listTarget.contains(row);
329
282
  }
330
- /**
331
- * Retires rows that left the live UI (list swap, external removal). A retired
332
- * clone is removed outright — generated rows only ever live under the current
333
- * `list`, so the before-cache rewind never has an untracked leftover to miss.
334
- */
335
283
  #prune() {
336
284
  for (const [id, row] of this.#rows) {
337
285
  if (this.#tracksRow(row)) continue;
338
- row.remove();
339
- this.#rows.delete(id);
286
+ this.#retire(id, row);
340
287
  }
341
288
  }
342
- /**
343
- * Rewinds for the snapshot and reports what that discarded. An upload in flight
344
- * cannot survive the navigation, so a consumer mirroring the rows would keep a
345
- * progress bar that never resolves.
346
- */
347
- #rewindForCache() {
348
- const ids = [...this.#rows.keys()];
349
- this.#reset();
350
- if (ids.length > 0) this.dispatch("reconcile", { detail: { ids } });
351
- }
352
- #reset() {
353
- for (const row of this.#rows.values()) row.remove();
354
- this.#rows.clear();
355
- this.#timeouts.clearAll();
356
- this.#syncAggregate();
289
+ #retire(id, row) {
290
+ row.remove();
291
+ this.#rows.delete(id);
292
+ this.#removals.clear(id);
357
293
  }
358
- /**
359
- * Sends one consumer-worded message to the page's shared announcer. `{name}`
360
- * resolves to the row's displayed name first — the event that settles an
361
- * upload may omit the file although an earlier event already named the row —
362
- * then the event's own name, then the accessible name (which an authored
363
- * label owns, so it is the last resort, not the primary source).
364
- */
365
294
  #announce(template, name, row) {
366
295
  const stored = this.#field(row, "name")?.textContent ?? "";
367
296
  const label = stored.length > 0 ? stored : name.length > 0 ? name : row.getAttribute("aria-label") ?? "";
@@ -377,17 +306,9 @@ var DirectUploadController = class extends Controller {
377
306
  #detail(event) {
378
307
  return event.detail ?? {};
379
308
  }
380
- /** The event's file name, or `""` when it carries none. */
381
309
  #name(detail) {
382
310
  return detail.file?.name ?? "";
383
311
  }
384
- /**
385
- * Whether an event belongs to this controller. With `scope` set, only events
386
- * whose target (the file input) sits inside an element matching `scope` are
387
- * handled, so several upload widgets on one page do not cross-populate.
388
- * Resolved with `closest()` from the target itself, so the chatty `progress`
389
- * stream never pays a document-wide query. Empty `scope` handles all.
390
- */
391
312
  #inScope(event) {
392
313
  if (this.#scopeSelector.length === 0) return true;
393
314
  const target = event.target;
@@ -399,5 +320,4 @@ var DirectUploadController = class extends Controller {
399
320
  };
400
321
 
401
322
  export { DirectUploadController };
402
- //# sourceMappingURL=direct_upload_controller.js.map
403
323
  //# sourceMappingURL=direct_upload_controller.js.map
@@ -1,68 +1,34 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/dirty_form_controller.ts
4
-
5
- // src/utils/listener_set.ts
6
3
  var ListenerSet = class {
7
- /** The generation every `add` joins until the next `dispose()`. */
8
4
  #abort = new AbortController();
9
- /**
10
- * Attaches `handler` to the open generation, exactly as the caller spelled it.
11
- *
12
- * The set supplies the signal, so `options` carries everything else the DOM
13
- * accepts — `capture` included, which has to match at release time and no
14
- * longer has a second place to drift from.
15
- */
16
5
  add(target, type, handler, options) {
17
6
  target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
18
7
  }
19
- /**
20
- * Releases every listener of the open generation, synchronously, and opens the
21
- * next one. Idempotent, and safe before anything has been added.
22
- */
23
8
  dispose() {
24
9
  this.#abort.abort();
25
10
  this.#abort = new AbortController();
26
11
  }
27
12
  };
28
13
 
29
- // src/utils/safe_timeout.ts
30
14
  var TimerRegistry = class {
31
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
32
15
  ids = /* @__PURE__ */ new Set();
33
- /**
34
- * Cancels a single tracked timer.
35
- *
36
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
37
- * registry), so callers can clear defensively without guarding.
38
- */
39
16
  clear(id) {
40
17
  if (this.ids.delete(id)) {
41
18
  this.cancel(id);
42
19
  }
43
20
  }
44
- /**
45
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
46
- * to guarantee no timer outlives the element.
47
- */
48
21
  clearAll() {
49
22
  for (const id of this.ids) {
50
23
  this.cancel(id);
51
24
  }
52
25
  this.ids.clear();
53
26
  }
54
- /** Number of timers currently tracked (pending). */
55
27
  get size() {
56
28
  return this.ids.size;
57
29
  }
58
30
  };
59
31
  var SafeTimeout = class extends TimerRegistry {
60
- /**
61
- * Schedules `callback` after `delay` ms and returns the timer id.
62
- *
63
- * The id is removed from the registry automatically when the timeout fires,
64
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
65
- */
66
32
  set(callback, delay) {
67
33
  const id = this.schedule(() => {
68
34
  this.ids.delete(id);
@@ -79,21 +45,31 @@ var SafeTimeout = class extends TimerRegistry {
79
45
  }
80
46
  };
81
47
 
82
- // src/utils/transient_hooks.ts
48
+ function sharedRegistry(key, create) {
49
+ const symbol = Symbol.for(key);
50
+ const scope = globalThis;
51
+ const existing = scope[symbol];
52
+ if (existing !== void 0) return existing;
53
+ const registry = create();
54
+ Object.defineProperty(globalThis, symbol, { value: registry });
55
+ return registry;
56
+ }
57
+
83
58
  var TransientHooks = class {
84
59
  #attributes;
85
60
  constructor(options) {
86
61
  this.#attributes = options.attributes;
87
62
  }
88
- /** Drops the declared hooks from `element`; what it carries besides stays. */
89
63
  reset(element) {
90
64
  for (const attribute of this.#attributes) element.removeAttribute(attribute);
91
65
  }
92
66
  };
93
67
 
94
- // src/controllers/dirty_form_controller.ts
95
68
  var TRANSIENT = new TransientHooks({ attributes: ["data-dirty"] });
96
- var visitCoordinators = /* @__PURE__ */ new WeakMap();
69
+ var visitCoordinators = sharedRegistry(
70
+ "stimeo-ui.dirty-form.registry.v1",
71
+ () => /* @__PURE__ */ new WeakMap()
72
+ );
97
73
  var TurboVisitCoordinator = class {
98
74
  #document;
99
75
  #participants = /* @__PURE__ */ new Set();
@@ -127,7 +103,6 @@ var TurboVisitCoordinator = class {
127
103
  }
128
104
  this.#participants.add(participant);
129
105
  }
130
- /** Removes a participant and reports whether the coordinator became empty. */
131
106
  remove(participant) {
132
107
  this.#participants.delete(participant);
133
108
  if (this.#participants.size > 0) return false;
@@ -163,7 +138,6 @@ var DirtyFormController = class extends Controller {
163
138
  #beforeunloadBound = false;
164
139
  #timeouts = new SafeTimeout();
165
140
  #listeners = new ListenerSet();
166
- /** Whether the active submission still represents the form's current values. */
167
141
  #submitting = false;
168
142
  #submitAttempt = null;
169
143
  #submittedBaseline = null;
@@ -216,7 +190,6 @@ var DirtyFormController = class extends Controller {
216
190
  event.returnValue = this.messageValue;
217
191
  };
218
192
  connect() {
219
- this.#resetSubmission();
220
193
  this.#baseline = this.#serialize();
221
194
  this.#setDirty(false);
222
195
  TRANSIENT.reset(this.element);
@@ -238,17 +211,14 @@ var DirtyFormController = class extends Controller {
238
211
  this.#unbindBeforeUnload();
239
212
  this.#resetSubmission();
240
213
  }
241
- /** Re-baselines to the current values and clears the dirty state (e.g. after a save). */
242
214
  markClean() {
243
215
  this.#baseline = this.#serialize();
244
216
  this.#setDirty(false);
245
217
  }
246
- /** Adopts restored values without erasing an already-dirty user revision. */
247
218
  acceptRestore() {
248
219
  if (this.#dirty) return;
249
220
  this.markClean();
250
221
  }
251
- /** Recomputes dirty against the baseline and flips state when it changes. */
252
222
  #evaluate() {
253
223
  this.#setDirty(this.#serialize() !== this.#baseline);
254
224
  }
@@ -264,7 +234,6 @@ var DirtyFormController = class extends Controller {
264
234
  }
265
235
  this.dispatch("dirty", { detail: { dirty } });
266
236
  }
267
- /** Evaluates this form for the document-level Turbo visit coordinator. */
268
237
  #evaluateVisit(event) {
269
238
  if (!this.#dirty || this.#guardSuppressed()) return null;
270
239
  const guard = this.dispatch("guard", { detail: { event }, cancelable: true });
@@ -293,7 +262,6 @@ var DirtyFormController = class extends Controller {
293
262
  this.#submittedBaseline = null;
294
263
  this.#activeSubmission = null;
295
264
  }
296
- /** Stable serialization of the form's controls for change detection. */
297
265
  #serialize() {
298
266
  const parts = [];
299
267
  for (const el of Array.from(this.element.elements)) {
@@ -310,7 +278,6 @@ var DirtyFormController = class extends Controller {
310
278
  }
311
279
  return JSON.stringify(parts);
312
280
  }
313
- /** A stable key for a control, or null for elements without value semantics. */
314
281
  #nameOf(el) {
315
282
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
316
283
  return el.name || el.id || "";
@@ -320,5 +287,4 @@ var DirtyFormController = class extends Controller {
320
287
  };
321
288
 
322
289
  export { DirtyFormController };
323
- //# sourceMappingURL=dirty_form_controller.js.map
324
290
  //# sourceMappingURL=dirty_form_controller.js.map