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,37 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/relative_time_controller.ts
4
-
5
- // src/utils/before_cache_reset.ts
6
- var BeforeCacheReset = class _BeforeCacheReset {
7
- /** Every subscribed instance, iterated by the one shared document listener. */
8
- static #subscribers = /* @__PURE__ */ new Set();
9
- /** The shared listener; installed while at least one instance is subscribed. */
10
- static #onBeforeCache = () => {
11
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
12
- };
13
- #rewind;
14
- /** @param rewind - the pass that returns this controller's state to its initial form. */
15
- constructor(rewind) {
16
- this.#rewind = rewind;
17
- }
18
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
19
- activate() {
20
- const first = _BeforeCacheReset.#subscribers.size === 0;
21
- _BeforeCacheReset.#subscribers.add(this);
22
- if (first) {
23
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
24
- }
25
- }
26
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
27
- deactivate() {
28
- _BeforeCacheReset.#subscribers.delete(this);
29
- if (_BeforeCacheReset.#subscribers.size > 0) return;
30
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
31
- }
32
- };
33
-
34
- // src/utils/intl_format.ts
35
3
  var formatters = /* @__PURE__ */ new WeakMap();
36
4
  function intlFormatter(factory, locale, options, fallbackLocale) {
37
5
  const cache = cacheFor(factory);
@@ -60,32 +28,37 @@ function construct(factory, locale, options, fallbackLocale) {
60
28
  }
61
29
  }
62
30
 
63
- // src/utils/locale.ts
64
31
  function resolveLocale(element, declared = "") {
65
32
  return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
66
33
  }
67
34
 
68
- // src/utils/microtask_coalescer.ts
35
+ var ListenerSet = class {
36
+ #abort = new AbortController();
37
+ add(target, type, handler, options) {
38
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
39
+ }
40
+ dispose() {
41
+ this.#abort.abort();
42
+ this.#abort = new AbortController();
43
+ }
44
+ };
45
+
69
46
  var MicrotaskCoalescer = class {
70
47
  #run;
71
48
  #queued = false;
72
49
  #active = false;
73
50
  #generation = 0;
74
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
75
51
  constructor(run) {
76
52
  this.#run = run;
77
53
  }
78
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
79
54
  activate() {
80
55
  this.#active = true;
81
56
  }
82
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
83
57
  cancel() {
84
58
  this.#active = false;
85
59
  this.#queued = false;
86
60
  this.#generation += 1;
87
61
  }
88
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
89
62
  schedule() {
90
63
  if (!this.#active || this.#queued) return;
91
64
  this.#queued = true;
@@ -98,43 +71,96 @@ var MicrotaskCoalescer = class {
98
71
  }
99
72
  };
100
73
 
101
- // src/utils/safe_timeout.ts
74
+ var MorphRenderWatcher = class {
75
+ #listeners = new ListenerSet();
76
+ #pass;
77
+ #element = null;
78
+ #onMorph = () => this.#pass.schedule();
79
+ constructor(run) {
80
+ this.#pass = new MicrotaskCoalescer(run);
81
+ }
82
+ observe(element) {
83
+ if (this.#element === element) return;
84
+ this.disconnect();
85
+ this.#element = element;
86
+ this.#pass.activate();
87
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
88
+ }
89
+ schedule() {
90
+ this.#pass.schedule();
91
+ }
92
+ disconnect() {
93
+ this.#listeners.dispose();
94
+ this.#pass.cancel();
95
+ this.#element = null;
96
+ }
97
+ };
98
+
99
+ var MAX_TIMER_DELAY_MS = 2147483647;
100
+
101
+ var NUMBER_BOUNDS = {
102
+ nonNegative: { finite: true, min: 0 },
103
+ timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
104
+ function matchesNumberBounds(value, bounds) {
105
+ if (!Number.isFinite(value)) {
106
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
107
+ if (direction === null) return false;
108
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
109
+ }
110
+ if (bounds.min !== void 0 && value < bounds.min) return false;
111
+ if (bounds.max !== void 0 && value > bounds.max) return false;
112
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
113
+ if (bounds.integer && !Number.isInteger(value)) return false;
114
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
115
+ return true;
116
+ }
117
+
118
+ function readNumber(raw, fallback, bounds) {
119
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
120
+ }
121
+
122
+ var NumberValueReader = class {
123
+ #lastRejected = /* @__PURE__ */ new Map();
124
+ read(owner, name, raw, fallback, bounds) {
125
+ const resolved = readNumber(raw, fallback, bounds);
126
+ if (matchesNumberBounds(raw, bounds)) {
127
+ this.#lastRejected.delete(name);
128
+ return resolved;
129
+ }
130
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
131
+ const literal = owner.element.getAttribute(attribute);
132
+ if (literal === null) {
133
+ this.#lastRejected.delete(name);
134
+ return resolved;
135
+ }
136
+ if (this.#lastRejected.get(name) !== literal) {
137
+ this.#lastRejected.set(name, literal);
138
+ console.warn(
139
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
140
+ );
141
+ }
142
+ return resolved;
143
+ }
144
+ };
145
+
102
146
  var TimerRegistry = class {
103
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
104
147
  ids = /* @__PURE__ */ new Set();
105
- /**
106
- * Cancels a single tracked timer.
107
- *
108
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
109
- * registry), so callers can clear defensively without guarding.
110
- */
111
148
  clear(id) {
112
149
  if (this.ids.delete(id)) {
113
150
  this.cancel(id);
114
151
  }
115
152
  }
116
- /**
117
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
118
- * to guarantee no timer outlives the element.
119
- */
120
153
  clearAll() {
121
154
  for (const id of this.ids) {
122
155
  this.cancel(id);
123
156
  }
124
157
  this.ids.clear();
125
158
  }
126
- /** Number of timers currently tracked (pending). */
127
159
  get size() {
128
160
  return this.ids.size;
129
161
  }
130
162
  };
131
163
  var SafeTimeout = class extends TimerRegistry {
132
- /**
133
- * Schedules `callback` after `delay` ms and returns the timer id.
134
- *
135
- * The id is removed from the registry automatically when the timeout fires,
136
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
137
- */
138
164
  set(callback, delay) {
139
165
  const id = this.schedule(() => {
140
166
  this.ids.delete(id);
@@ -151,7 +177,6 @@ var SafeTimeout = class extends TimerRegistry {
151
177
  }
152
178
  };
153
179
 
154
- // src/controllers/relative_time_controller.ts
155
180
  var YEAR_SCALE = { limit: Number.POSITIVE_INFINITY, unit: "year", ms: 315576e5 };
156
181
  var UNITS = [
157
182
  { limit: 60, unit: "second", ms: 1e3 },
@@ -163,112 +188,105 @@ var UNITS = [
163
188
  YEAR_SCALE
164
189
  ];
165
190
  var RELATIVE_OPTIONS = { numeric: "auto" };
166
- var RelativeTimeController = class extends Controller {
191
+ var TEXT_RECORD = "text";
192
+ var RelativeTimeController = class _RelativeTimeController extends Controller {
193
+ #numbers = new NumberValueReader();
167
194
  static values = {
168
195
  locale: { type: String, default: "" },
169
196
  threshold: { type: Number, default: 0 },
170
197
  tickInterval: { type: Number, default: 6e4 }
171
198
  };
199
+ static valueConstraints = {
200
+ threshold: NUMBER_BOUNDS.nonNegative,
201
+ tickInterval: NUMBER_BOUNDS.timer
202
+ };
172
203
  #timers = new SafeTimeout();
173
- /** Collapses a morph that swaps several render inputs at once into one repaint. */
174
- #resync = new MicrotaskCoalescer(() => this.#resyncToValues());
175
- #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
176
- /** Epoch ms parsed from `datetime`; `NaN` when absent or invalid. */
204
+ #resync = new MorphRenderWatcher(() => {
205
+ if (this.#valuesDirty) {
206
+ this.#valuesDirty = false;
207
+ this.#resyncToValues();
208
+ } else if (!Number.isNaN(this.#targetMs)) {
209
+ this.#applyAndComputeDelay();
210
+ }
211
+ });
212
+ #valuesDirty = false;
177
213
  #targetMs = Number.NaN;
178
- /** The authored absolute text, restored when the threshold fallback kicks in. */
179
214
  #absoluteText = "";
180
215
  connect() {
181
- this.#resync.activate();
182
- this.#beforeCache.activate();
216
+ this.#valuesDirty = false;
217
+ this.#resync.observe(this.element);
183
218
  if (this.element.getAttribute("data-state") !== "relative") {
184
219
  this.#absoluteText = (this.element.textContent ?? "").trim();
220
+ } else {
221
+ const recorded = this.#recordedText();
222
+ if (recorded !== null) this.#absoluteText = recorded;
185
223
  }
186
224
  this.#targetMs = Date.parse(this.element.getAttribute("datetime") ?? "");
187
225
  if (Number.isNaN(this.#targetMs)) return;
188
226
  this.#schedule();
189
227
  }
190
228
  disconnect() {
191
- this.#resync.cancel();
192
- this.#beforeCache.deactivate();
229
+ this.#resync.disconnect();
193
230
  this.#timers.clearAll();
194
231
  }
195
- /** Repaints when application code (or a Turbo morph) changes `locale` at runtime. */
232
+ get #textRecord() {
233
+ return `data-${this.identifier}-${TEXT_RECORD}`;
234
+ }
235
+ #recordedText() {
236
+ try {
237
+ const parsed = JSON.parse(this.element.getAttribute(this.#textRecord) ?? "null");
238
+ return typeof parsed === "string" ? parsed : null;
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
196
243
  localeValueChanged() {
244
+ this.#valuesDirty = true;
197
245
  this.#resync.schedule();
198
246
  }
199
- /** Repaints when application code (or a Turbo morph) changes `threshold` at runtime. */
200
247
  thresholdValueChanged() {
248
+ this.#valuesDirty = true;
201
249
  this.#resync.schedule();
202
250
  }
203
- /** Repaints when application code (or a Turbo morph) changes `tickInterval` at runtime. */
204
251
  tickIntervalValueChanged() {
252
+ this.#valuesDirty = true;
205
253
  this.#resync.schedule();
206
254
  }
207
- /**
208
- * Renders against the current Values and re-arms the poll from now.
209
- *
210
- * Render only: it emits no event, and clearing first keeps the single self-arming
211
- * timer single — scheduling on top of a pending one would double the poll rate for
212
- * the rest of the session. A stamp whose `datetime` never parsed has nothing to
213
- * render, and one that already reached its terminal fallback simply renders it
214
- * again and stops.
215
- */
216
255
  #resyncToValues() {
217
256
  if (Number.isNaN(this.#targetMs)) return;
218
257
  this.#timers.clearAll();
219
258
  this.#schedule();
220
259
  }
221
- /**
222
- * Restores the authored absolute text and the pre-render state for the snapshot
223
- * Turbo is about to take, leaving the live page's poll timer alone.
224
- *
225
- * With no authored text held there is nothing to restore, and `data-state` has to
226
- * stay as it is: that marker is what tells the next `connect()` the visible text is
227
- * a rendered relative form rather than an absolute fallback to hold on to.
228
- */
229
- #rewindForCache() {
230
- if (!this.#absoluteText) return;
231
- this.element.textContent = this.#absoluteText;
232
- this.element.removeAttribute("data-state");
233
- }
234
- /** Renders the current representation and reschedules unless polling can stop. */
235
260
  #schedule() {
236
261
  const nextDelay = this.#applyAndComputeDelay();
237
262
  if (nextDelay !== null) {
238
263
  this.#timers.set(() => this.#schedule(), nextDelay);
239
264
  }
240
265
  }
241
- /**
242
- * Updates the visible text and returns the next poll delay (ms), or `null` when
243
- * polling can stop: a *past* timestamp that fell back to the absolute text can
244
- * never leave it again, and a locale the runtime rejects has nothing to render
245
- * until that value is corrected.
246
- *
247
- * @stimeoRenderRoot
248
- */
249
266
  #applyAndComputeDelay() {
250
267
  const deltaMs = this.#targetMs - Date.now();
251
268
  const absSeconds = Math.abs(deltaMs) / 1e3;
252
269
  const scale = UNITS.find((u) => absSeconds < u.limit) ?? YEAR_SCALE;
253
270
  const unitFloor = scale.unit === "second" || scale.unit === "minute" ? 6e4 : scale.ms;
254
- const nextDelay = Math.max(this.tickIntervalValue, Math.min(unitFloor, 864e5));
255
- if (this.thresholdValue > 0 && absSeconds >= this.thresholdValue && this.#absoluteText) {
271
+ const nextDelay = Math.max(this.#safeTickInterval, Math.min(unitFloor, 864e5));
272
+ if (this.#safeThreshold > 0 && absSeconds >= this.#safeThreshold && this.#absoluteText) {
256
273
  this.element.textContent = this.#absoluteText;
257
274
  this.element.setAttribute("data-state", "absolute");
275
+ this.element.removeAttribute(this.#textRecord);
258
276
  if (deltaMs <= 0) return null;
259
- return Math.min(nextDelay, deltaMs - this.thresholdValue * 1e3 + 1);
277
+ return Math.max(1, Math.min(nextDelay, deltaMs - this.#safeThreshold * 1e3 + 1));
260
278
  }
261
279
  const formatter = this.#formatter;
262
280
  if (formatter === null) return null;
263
281
  const value = Math.round(deltaMs / scale.ms);
264
282
  this.element.textContent = formatter.format(value, scale.unit);
265
283
  this.element.setAttribute("data-state", "relative");
284
+ const record = JSON.stringify(this.#absoluteText);
285
+ if (this.element.getAttribute(this.#textRecord) !== record) {
286
+ this.element.setAttribute(this.#textRecord, record);
287
+ }
266
288
  return nextDelay;
267
289
  }
268
- /**
269
- * A `RelativeTimeFormat` for the resolved locale (`numeric: "auto"`), or `null`
270
- * when the runtime rejects that locale.
271
- */
272
290
  get #formatter() {
273
291
  return intlFormatter(
274
292
  Intl.RelativeTimeFormat,
@@ -276,8 +294,25 @@ var RelativeTimeController = class extends Controller {
276
294
  RELATIVE_OPTIONS
277
295
  );
278
296
  }
297
+ get #safeThreshold() {
298
+ return this.#numbers.read(
299
+ this,
300
+ "threshold",
301
+ this.thresholdValue,
302
+ _RelativeTimeController.values.threshold.default,
303
+ _RelativeTimeController.valueConstraints.threshold
304
+ );
305
+ }
306
+ get #safeTickInterval() {
307
+ return this.#numbers.read(
308
+ this,
309
+ "tickInterval",
310
+ this.tickIntervalValue,
311
+ _RelativeTimeController.values.tickInterval.default,
312
+ _RelativeTimeController.valueConstraints.tickInterval
313
+ );
314
+ }
279
315
  };
280
316
 
281
317
  export { RelativeTimeController };
282
- //# sourceMappingURL=relative_time_controller.js.map
283
318
  //# sourceMappingURL=relative_time_controller.js.map
@@ -1,8 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/reset_before_cache_controller.ts
4
-
5
- // src/utils/declared_value.ts
6
3
  function parseDeclared(raw, parse, fallback) {
7
4
  try {
8
5
  return parse(raw);
@@ -22,7 +19,94 @@ function validSelector(element, raw, fallback) {
22
19
  );
23
20
  }
24
21
 
25
- // src/controllers/reset_before_cache_controller.ts
22
+ var ListenerSet = class {
23
+ #abort = new AbortController();
24
+ add(target, type, handler, options) {
25
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
26
+ }
27
+ dispose() {
28
+ this.#abort.abort();
29
+ this.#abort = new AbortController();
30
+ }
31
+ };
32
+
33
+ var MicrotaskCoalescer = class {
34
+ #run;
35
+ #queued = false;
36
+ #active = false;
37
+ #generation = 0;
38
+ constructor(run) {
39
+ this.#run = run;
40
+ }
41
+ activate() {
42
+ this.#active = true;
43
+ }
44
+ cancel() {
45
+ this.#active = false;
46
+ this.#queued = false;
47
+ this.#generation += 1;
48
+ }
49
+ schedule() {
50
+ if (!this.#active || this.#queued) return;
51
+ this.#queued = true;
52
+ const generation = this.#generation;
53
+ queueMicrotask(() => {
54
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
55
+ this.#queued = false;
56
+ this.#run();
57
+ });
58
+ }
59
+ };
60
+
61
+ var MorphRenderWatcher = class {
62
+ #listeners = new ListenerSet();
63
+ #pass;
64
+ #element = null;
65
+ #onMorph = () => this.#pass.schedule();
66
+ constructor(run) {
67
+ this.#pass = new MicrotaskCoalescer(run);
68
+ }
69
+ observe(element) {
70
+ if (this.#element === element) return;
71
+ this.disconnect();
72
+ this.#element = element;
73
+ this.#pass.activate();
74
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
75
+ }
76
+ schedule() {
77
+ this.#pass.schedule();
78
+ }
79
+ disconnect() {
80
+ this.#listeners.dispose();
81
+ this.#pass.cancel();
82
+ this.#element = null;
83
+ }
84
+ };
85
+
86
+ var LivedMark = class {
87
+ #name;
88
+ #lived = false;
89
+ #element = null;
90
+ #morph = new MorphRenderWatcher(() => this.#write());
91
+ constructor(identifier) {
92
+ this.#name = `data-${identifier}-lived`;
93
+ }
94
+ connect(element) {
95
+ const connection = this.#lived ? "reconnect" : element.hasAttribute(this.#name) ? "restored" : "fresh";
96
+ this.#lived = true;
97
+ this.#element = element;
98
+ this.#write();
99
+ this.#morph.observe(element);
100
+ return connection;
101
+ }
102
+ disconnect() {
103
+ this.#morph.disconnect();
104
+ }
105
+ #write() {
106
+ this.#element?.setAttribute(this.#name, "");
107
+ }
108
+ };
109
+
26
110
  var STATELESS_INPUT_TYPES = /* @__PURE__ */ new Set(["hidden", "submit", "reset", "button", "image"]);
27
111
  function restoreField(element) {
28
112
  if (element instanceof HTMLTextAreaElement) {
@@ -43,48 +127,25 @@ function restoreField(element) {
43
127
  }
44
128
  }
45
129
  }
46
- var ResetBeforeCacheController = class extends Controller {
130
+ var ResetOnRestoreController = class extends Controller {
47
131
  static values = {
48
- scope: { type: String, default: "" },
49
- dispatchReset: { type: Boolean, default: true }
132
+ scope: { type: String, default: "" }
50
133
  };
51
134
  static actions = ["reset"];
52
- static events = ["reset", "request"];
53
- /** The `scope` declaration after validation; empty when it cannot be parsed. */
135
+ static events = ["reset"];
54
136
  #scopeSelector = "";
55
- /** Runs the reset just before Turbo caches the snapshot. */
56
- #onBeforeCache = () => this.reset();
57
- /**
58
- * Validates the scope declaration once, keeping only a selector the engine can
59
- * read.
60
- *
61
- * A selector reads back as an ordinary string, so a malformed one survives
62
- * until it is handed to the DOM — and this part runs from a single listener
63
- * whose whole job is to keep a cached page from freezing. Falling back to the
64
- * default keeps that job running with a visible, findable result instead of
65
- * silently taking the sweep down.
66
- */
137
+ #lived = new LivedMark(this.identifier);
67
138
  scopeValueChanged() {
68
139
  this.#scopeSelector = validSelector(this.element, this.scopeValue, "");
69
140
  }
70
141
  connect() {
71
- document.addEventListener("turbo:before-cache", this.#onBeforeCache);
142
+ if (this.#lived.connect(this.element) === "restored") this.reset();
72
143
  }
73
144
  disconnect() {
74
- document.removeEventListener("turbo:before-cache", this.#onBeforeCache);
75
- }
76
- /**
77
- * Resets transient UI within scope to its initial state. Asks controllers to
78
- * close (via `request`) first, then applies the declarative `data-reset-*` cleanup,
79
- * and finally emits `reset`. Safe to call any number of times (idempotent).
80
- *
81
- * `dispatchReset` decides only whether the `request` ask goes out; the cleanup
82
- * and the closing `reset` run either way. Both the listener and this action
83
- * reach the same sweep, so `reset` is emitted for a manual call too.
84
- */
145
+ this.#lived.disconnect();
146
+ }
85
147
  reset() {
86
148
  const root = this.#scopeRoot();
87
- if (this.dispatchResetValue) this.dispatch("request");
88
149
  for (const element of root.querySelectorAll("[data-reset-attr]")) {
89
150
  for (const name of (element.getAttribute("data-reset-attr") ?? "").split(/\s+/)) {
90
151
  if (name) element.removeAttribute(name);
@@ -109,13 +170,11 @@ var ResetBeforeCacheController = class extends Controller {
109
170
  }
110
171
  this.dispatch("reset");
111
172
  }
112
- /** The scan root: a `scope` descendant when set, else the controller element. */
113
173
  #scopeRoot() {
114
174
  if (!this.#scopeSelector) return this.element;
115
175
  return this.element.querySelector(this.#scopeSelector) ?? this.element;
116
176
  }
117
177
  };
118
178
 
119
- export { ResetBeforeCacheController };
120
- //# sourceMappingURL=reset_before_cache_controller.js.map
121
- //# sourceMappingURL=reset_before_cache_controller.js.map
179
+ export { ResetOnRestoreController };
180
+ //# sourceMappingURL=reset_on_restore_controller.js.map