stimeo-ui 0.15.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 +209 -0
  3. data/README.md +128 -1
  4. data/dist/cable/index.js +392 -417
  5. data/dist/controllers/accordion_controller.js +228 -28
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -191
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +295 -110
  10. data/dist/controllers/avatar_controller.js +172 -106
  11. data/dist/controllers/breadcrumb_controller.js +0 -129
  12. data/dist/controllers/bulk_select_controller.js +264 -53
  13. data/dist/controllers/calendar_controller.js +346 -180
  14. data/dist/controllers/carousel_controller.js +587 -300
  15. data/dist/controllers/character_counter_controller.js +271 -143
  16. data/dist/controllers/checkbox_controller.js +77 -75
  17. data/dist/controllers/clipboard_controller.js +187 -108
  18. data/dist/controllers/collapsible_controller.js +374 -108
  19. data/dist/controllers/color_picker_controller.js +183 -127
  20. data/dist/controllers/combobox_controller.js +426 -109
  21. data/dist/controllers/command_palette_controller.js +994 -350
  22. data/dist/controllers/conditional_fields_controller.js +212 -125
  23. data/dist/controllers/confirm_controller.js +939 -245
  24. data/dist/controllers/context_menu_controller.js +275 -127
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +235 -97
  27. data/dist/controllers/currency_input_controller.js +241 -118
  28. data/dist/controllers/data_grid_controller.js +229 -135
  29. data/dist/controllers/date_range_picker_controller.js +359 -165
  30. data/dist/controllers/dialog_controller.js +887 -225
  31. data/dist/controllers/direct_upload_controller.js +80 -142
  32. data/dist/controllers/dirty_form_controller.js +26 -47
  33. data/dist/controllers/dismissible_controller.js +175 -20
  34. data/dist/controllers/drawer_controller.js +963 -336
  35. data/dist/controllers/dropdown_controller.js +342 -85
  36. data/dist/controllers/editable_controller.js +182 -58
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +426 -229
  39. data/dist/controllers/filter_controller.js +78 -36
  40. data/dist/controllers/flash_controller.js +328 -211
  41. data/dist/controllers/focus_controller.js +654 -239
  42. data/dist/controllers/form_field_controller.js +153 -136
  43. data/dist/controllers/form_validation_controller.js +29 -96
  44. data/dist/controllers/frame_loading_controller.js +263 -198
  45. data/dist/controllers/highlight_controller.js +119 -70
  46. data/dist/controllers/hover_card_controller.js +335 -118
  47. data/dist/controllers/idle_controller.js +316 -65
  48. data/dist/controllers/input_mask_controller.js +116 -61
  49. data/dist/controllers/intersection_controller.js +147 -103
  50. data/dist/controllers/lazy_frame_controller.js +9 -56
  51. data/dist/controllers/listbox_controller.js +413 -153
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -95
  54. data/dist/controllers/menu_controller.js +254 -167
  55. data/dist/controllers/menubar_controller.js +206 -331
  56. data/dist/controllers/meter_controller.js +159 -71
  57. data/dist/controllers/multi_select_controller.js +797 -388
  58. data/dist/controllers/navigation_menu_controller.js +156 -231
  59. data/dist/controllers/nested_form_controller.js +373 -174
  60. data/dist/controllers/network_status_controller.js +241 -45
  61. data/dist/controllers/number_input_controller.js +387 -227
  62. data/dist/controllers/optimistic_controller.js +28 -75
  63. data/dist/controllers/otp_controller.js +344 -196
  64. data/dist/controllers/overflow_indicator_controller.js +167 -93
  65. data/dist/controllers/overflow_menu_controller.js +408 -277
  66. data/dist/controllers/pagination_controller.js +342 -120
  67. data/dist/controllers/password_reveal_controller.js +424 -99
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +252 -96
  70. data/dist/controllers/pointer_drag_controller.js +263 -164
  71. data/dist/controllers/popover_controller.js +267 -94
  72. data/dist/controllers/portal_controller.js +209 -72
  73. data/dist/controllers/preview_guard_controller.js +243 -112
  74. data/dist/controllers/progress_controller.js +132 -59
  75. data/dist/controllers/radio_group_controller.js +138 -133
  76. data/dist/controllers/range_slider_controller.js +245 -112
  77. data/dist/controllers/rating_controller.js +429 -194
  78. data/dist/controllers/read_more_controller.js +185 -47
  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 +294 -118
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -221
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -146
  87. data/dist/controllers/scrollspy_controller.js +181 -236
  88. data/dist/controllers/separator_controller.js +288 -132
  89. data/dist/controllers/sidebar_controller.js +1071 -375
  90. data/dist/controllers/skeleton_controller.js +248 -92
  91. data/dist/controllers/slider_controller.js +211 -96
  92. data/dist/controllers/smart_sticky_header_controller.js +163 -54
  93. data/dist/controllers/sortable_controller.js +13 -110
  94. data/dist/controllers/spinner_controller.js +277 -161
  95. data/dist/controllers/step_indicator_controller.js +107 -53
  96. data/dist/controllers/stepper_controller.js +171 -48
  97. data/dist/controllers/stick_to_bottom_controller.js +310 -116
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -192
  100. data/dist/controllers/switch_controller.js +107 -43
  101. data/dist/controllers/tabs_controller.js +50 -25
  102. data/dist/controllers/tags_input_controller.js +290 -149
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +148 -80
  105. data/dist/controllers/time_picker_controller.js +162 -81
  106. data/dist/controllers/toast_controller.js +365 -193
  107. data/dist/controllers/toggle_group_controller.js +194 -99
  108. data/dist/controllers/toolbar_controller.js +92 -90
  109. data/dist/controllers/tooltip_controller.js +185 -125
  110. data/dist/controllers/transition_controller.js +123 -115
  111. data/dist/controllers/tree_view_controller.js +248 -290
  112. data/dist/index.js +8649 -8572
  113. data/dist/positioning/index.js +148 -45
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,55 +1,218 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/editable_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 registry = create();
9
+ Object.defineProperty(globalThis, symbol, { value: registry });
10
+ return registry;
11
+ }
4
12
 
5
- // src/utils/composition_tracker.ts
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
+ var AttributeLease = class _AttributeLease {
30
+ static #registry = sharedRegistry(
31
+ "stimeo-ui.attribute-lease.registry.v1",
32
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
33
+ );
34
+ #attribute;
35
+ #record;
36
+ #base;
37
+ #held = /* @__PURE__ */ new Set();
38
+ constructor(attribute, owner, options = {}) {
39
+ this.#attribute = attribute;
40
+ this.#record = `data-${owner}-${attribute}-lease`;
41
+ this.#base = options.base === true;
42
+ const names = _AttributeLease.#registry.names;
43
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
44
+ }
45
+ write(element, value) {
46
+ const stack = this.#stack(element);
47
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
48
+ this.#held.add(element);
49
+ const shows = !this.#covered(element, stack, hold);
50
+ hold.written = value;
51
+ this.#mark(element, stack);
52
+ if (shows) this.#reflect(element, value);
53
+ }
54
+ return(element) {
55
+ const held = this.#held.delete(element);
56
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
57
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
58
+ const hold = this.#take(stack);
59
+ if (!hold) return false;
60
+ const carries = element.getAttribute(this.#attribute) === hold.written;
61
+ const owns = carries && !this.#covered(element, stack, hold);
62
+ const index = stack.indexOf(hold);
63
+ stack.splice(index, 1);
64
+ const above = stack[index];
65
+ if (above) above.linked &&= hold.linked;
66
+ this.#mark(element, stack);
67
+ if (!owns) return true;
68
+ const below = hold.linked ? stack[index - 1] : void 0;
69
+ this.#reflect(element, below ? below.written : hold.original);
70
+ return true;
71
+ }
72
+ returnAll() {
73
+ for (const element of Array.from(this.#held)) this.return(element);
74
+ }
75
+ #stack(element) {
76
+ const stacks = _AttributeLease.#registry.stacks;
77
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
78
+ stacks.set(element, byAttribute);
79
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
80
+ byAttribute.set(this.#attribute, stack);
81
+ return stack;
82
+ }
83
+ #seed(element) {
84
+ const found = [];
85
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
86
+ const read = readLeaseRecord(element, record);
87
+ if (read) found.push({ ...read, record });
88
+ }
89
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
90
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
91
+ lease: null,
92
+ record,
93
+ written,
94
+ original,
95
+ linked: linked && index > 0
96
+ }));
97
+ }
98
+ #take(stack) {
99
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
100
+ if (hold) hold.lease = this;
101
+ return hold;
102
+ }
103
+ #acquire(element, stack) {
104
+ const current = element.getAttribute(this.#attribute);
105
+ const top = stack.at(-1);
106
+ const bottom = stack[0];
107
+ const over = top?.written === current ? top : void 0;
108
+ const from = this.#base ? bottom : over;
109
+ const hold = {
110
+ lease: this,
111
+ record: this.#record,
112
+ written: current,
113
+ original: from ? from.original : current,
114
+ linked: !this.#base && over !== void 0
115
+ };
116
+ if (this.#base && bottom) {
117
+ bottom.linked = true;
118
+ stack.unshift(hold);
119
+ } else stack.push(hold);
120
+ return hold;
121
+ }
122
+ #covered(element, stack, hold) {
123
+ const current = element.getAttribute(this.#attribute);
124
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
125
+ }
126
+ #mark(element, stack) {
127
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
128
+ stack.forEach((hold, index) => {
129
+ const names = stack.slice(0, index).map((under) => under.record);
130
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
131
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
132
+ records.set(
133
+ hold.record,
134
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
135
+ );
136
+ });
137
+ for (const [name, recorded] of records) {
138
+ if (element.getAttribute(name) === recorded) continue;
139
+ if (recorded === null) element.removeAttribute(name);
140
+ else element.setAttribute(name, recorded);
141
+ }
142
+ }
143
+ #reflect(element, value) {
144
+ if (element.getAttribute(this.#attribute) === value) return;
145
+ if (value === null) element.removeAttribute(this.#attribute);
146
+ else element.setAttribute(this.#attribute, value);
147
+ }
148
+ };
149
+
150
+ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
151
+ "insertCompositionText",
152
+ "insertFromComposition",
153
+ "deleteCompositionText",
154
+ "deleteByComposition"
155
+ ]);
6
156
  var CompositionTracker = class {
7
157
  #observedTargets = /* @__PURE__ */ new Set();
8
158
  #activeTargets = /* @__PURE__ */ new Set();
9
159
  #onStart;
10
160
  #onEnd;
161
+ #confirmedTarget = null;
11
162
  constructor(options = {}) {
12
163
  this.#onStart = options.onStart;
13
164
  this.#onEnd = options.onEnd;
14
165
  }
15
- /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
16
166
  observe(target) {
17
167
  if (this.#observedTargets.has(target)) return;
18
168
  target.addEventListener("compositionstart", this.#handleStart);
19
169
  target.addEventListener("compositionend", this.#handleEnd);
170
+ target.addEventListener("keydown", this.#handleKeydown);
20
171
  this.#observedTargets.add(target);
21
172
  }
22
- /** Stops tracking one target and clears any active composition it owned. */
23
173
  unobserve(target) {
24
174
  if (!this.#observedTargets.delete(target)) return;
25
175
  target.removeEventListener("compositionstart", this.#handleStart);
26
176
  target.removeEventListener("compositionend", this.#handleEnd);
177
+ target.removeEventListener("keydown", this.#handleKeydown);
27
178
  this.#activeTargets.delete(target);
179
+ if (this.#confirmedTarget === target) this.#confirmedTarget = null;
28
180
  }
29
- /** Releases every listener and clears state so reconnect starts cleanly. */
30
181
  disconnect() {
31
182
  for (const target of this.#observedTargets) {
32
183
  target.removeEventListener("compositionstart", this.#handleStart);
33
184
  target.removeEventListener("compositionend", this.#handleEnd);
185
+ target.removeEventListener("keydown", this.#handleKeydown);
34
186
  }
35
187
  this.#observedTargets.clear();
36
188
  this.#activeTargets.clear();
189
+ this.#confirmedTarget = null;
37
190
  }
38
- /** True when lifecycle tracking or the current event reports composition. */
39
191
  isComposing(event) {
40
192
  return this.#activeTargets.size > 0 || event?.isComposing === true;
41
193
  }
194
+ consumesConfirmedInput(event) {
195
+ const confirmed = this.#confirmedTarget;
196
+ this.#confirmedTarget = null;
197
+ if (confirmed === null || confirmed !== event.target) return false;
198
+ const inputType = event.inputType;
199
+ return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
200
+ }
42
201
  #handleStart = (event) => {
202
+ this.#confirmedTarget = null;
43
203
  if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
44
204
  this.#onStart?.(event);
45
205
  };
46
206
  #handleEnd = (event) => {
47
207
  if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
208
+ this.#confirmedTarget = event.target;
48
209
  this.#onEnd?.(event);
49
210
  };
211
+ #handleKeydown = () => {
212
+ this.#confirmedTarget = null;
213
+ };
50
214
  };
51
215
 
52
- // src/controllers/editable_controller.ts
53
216
  var EditableController = class extends Controller {
54
217
  static targets = ["display", "input"];
55
218
  static values = {
@@ -57,29 +220,11 @@ var EditableController = class extends Controller {
57
220
  };
58
221
  static actions = ["cancel", "edit", "onDisplayKeydown", "onKeydown", "revert", "save"];
59
222
  static events = ["cancel", "change"];
60
- /** The value captured when edit mode began, used to detect real changes. */
61
223
  #previousValue = "";
62
- /**
63
- * The value the last save replaced, or `null` when there is nothing to undo.
64
- * Cleared on connect: a value from before a page restore is unrecoverable, so
65
- * `revert()` must not resurrect one.
66
- */
67
224
  #revertValue = null;
68
- /**
69
- * Owns IME lifecycle state for the edit surface, so a keydown that belongs to
70
- * a composition (cancel or confirm) is never treated as an edit command.
71
- */
72
225
  #composition = new CompositionTracker();
73
- /**
74
- * Watches focus leaving the editor from wherever it currently sits.
75
- *
76
- * `focusout` bubbles where `blur` does not, so one listener on the root sees
77
- * every departure — including one from a Save or Cancel button the consumer
78
- * placed beside the input. Binding the input alone would make the promise
79
- * "saves wherever focus moved" true only for focus that leaves the input
80
- * itself, and tabbing straight past an inner button would strand the editor
81
- * open.
82
- */
226
+ #hidden = new AttributeLease("hidden", this.identifier);
227
+ #connected = false;
83
228
  #onFocusOut = (event) => {
84
229
  const from = event.target;
85
230
  if (this.hasDisplayTarget && from instanceof Node && this.displayTarget.contains(from)) return;
@@ -87,32 +232,34 @@ var EditableController = class extends Controller {
87
232
  if (next instanceof Node && this.element.contains(next)) return;
88
233
  if (this.submitOnBlurValue) this.#commit(false);
89
234
  };
90
- /** Establishes the initial display mode (display shown, input hidden). */
91
235
  connect() {
236
+ this.#connected = true;
92
237
  if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
93
238
  this.element.addEventListener("focusout", this.#onFocusOut);
94
239
  this.#revertValue = null;
95
240
  this.#setMode("display");
96
241
  }
97
- /** Releases the composition and focus listeners so nothing outlives the element. */
98
242
  disconnect() {
243
+ this.#connected = false;
99
244
  this.#composition.disconnect();
100
245
  this.element.removeEventListener("focusout", this.#onFocusOut);
101
246
  }
102
- /** Tracks an input added initially or after connect (e.g. a Turbo swap). */
103
247
  inputTargetConnected(input) {
104
248
  this.#composition.observe(input);
105
249
  this.#applyMode();
106
250
  }
107
- /** Removes composition listeners when the active input is replaced or removed. */
108
251
  inputTargetDisconnected(input) {
109
252
  this.#composition.unobserve(input);
253
+ if (!this.inputTargets.includes(input)) this.#hidden.return(input);
254
+ if (this.#connected) this.#applyMode();
110
255
  }
111
- /** Re-hides or re-shows a display element that arrived after the mode was set. */
112
256
  displayTargetConnected() {
113
257
  this.#applyMode();
114
258
  }
115
- /** Enters edit mode: seeds the input from the declared value, focuses, selects. */
259
+ displayTargetDisconnected(display) {
260
+ if (!this.displayTargets.includes(display)) this.#hidden.return(display);
261
+ if (this.#connected) this.#applyMode();
262
+ }
116
263
  edit() {
117
264
  if (this.#isEditing || !this.hasInputTarget || !this.hasDisplayTarget) return;
118
265
  this.#previousValue = this.#currentValue;
@@ -121,35 +268,26 @@ var EditableController = class extends Controller {
121
268
  this.inputTarget.focus();
122
269
  this.inputTarget.select();
123
270
  }
124
- /** Commits the edit and returns focus to the display element. */
125
271
  save() {
126
272
  this.#commit(true);
127
273
  }
128
- /** Discards edits, returns to display mode, and dispatches `cancel`. */
129
274
  cancel() {
130
275
  if (!this.#isEditing) return;
131
276
  this.#setMode("display");
132
277
  if (this.hasDisplayTarget) this.displayTarget.focus();
133
278
  this.dispatch("cancel", { detail: {} });
134
279
  }
135
- /**
136
- * Puts back the value the last save replaced — for a consumer whose server
137
- * rejected it. Silent by design: a `change` here would re-enter the same
138
- * handler that asked for the undo. One save, one undo.
139
- */
140
280
  revert() {
141
281
  if (this.#revertValue === null || this.#isEditing) return;
142
282
  this.#writeValue(this.#revertValue);
143
283
  this.#revertValue = null;
144
284
  }
145
- /** Adds `F2` as an editing entry point alongside the button's native activation. */
146
285
  onDisplayKeydown(event) {
147
286
  if (event.key === "F2") {
148
287
  event.preventDefault();
149
288
  this.edit();
150
289
  }
151
290
  }
152
- /** Commits on `Enter` (or `Ctrl+Enter` when multiline) and cancels on `Escape`. */
153
291
  onKeydown(event) {
154
292
  if (this.#composition.isComposing(event)) return;
155
293
  if (event.key === "Escape") {
@@ -165,13 +303,6 @@ var EditableController = class extends Controller {
165
303
  this.#commit(true);
166
304
  }
167
305
  }
168
- /**
169
- * Returns to display mode, storing the input's value and dispatching `change`
170
- * when it differs from where editing began.
171
- *
172
- * @param restoreFocus - Move focus back to the display element (explicit
173
- * commit) rather than honoring the user's new focus target (blur).
174
- */
175
306
  #commit(restoreFocus) {
176
307
  if (!this.#isEditing || !this.hasInputTarget) return;
177
308
  const value = this.inputTarget.value.trim();
@@ -184,39 +315,32 @@ var EditableController = class extends Controller {
184
315
  this.dispatch("change", { detail: { value, previous } });
185
316
  }
186
317
  }
187
- /** Stores `value` wherever the display element declares it. */
188
318
  #writeValue(value) {
189
319
  if (!this.hasDisplayTarget) return;
190
320
  const display = this.displayTarget;
191
321
  if (display.hasAttribute("data-value")) display.dataset.value = value;
192
322
  else display.textContent = value;
193
323
  }
194
- /** Derives both elements' visibility from the mode currently in the DOM. */
195
324
  #applyMode() {
196
325
  const editing = this.#isEditing;
197
- if (this.hasDisplayTarget) this.displayTarget.hidden = editing;
198
- if (this.hasInputTarget) this.inputTarget.hidden = !editing;
326
+ if (this.hasDisplayTarget) this.#hidden.write(this.displayTarget, editing ? "" : null);
327
+ if (this.hasInputTarget) this.#hidden.write(this.inputTarget, editing ? null : "");
199
328
  }
200
- /** Records the mode, then brings both elements in line with it. */
201
329
  #setMode(mode) {
202
330
  this.element.dataset.mode = mode;
203
331
  this.#applyMode();
204
332
  }
205
- /** The value the display element declares, trimmed. */
206
333
  get #currentValue() {
207
334
  const display = this.displayTarget;
208
335
  return (display.dataset.value ?? display.textContent ?? "").trim();
209
336
  }
210
- /** Whether the editing control is a multi-line `<textarea>`. */
211
337
  get #isMultiline() {
212
338
  return this.inputTarget.tagName === "TEXTAREA";
213
339
  }
214
- /** Whether the controller is currently in edit mode. */
215
340
  get #isEditing() {
216
341
  return this.element.dataset.mode === "editing";
217
342
  }
218
343
  };
219
344
 
220
345
  export { EditableController };
221
- //# sourceMappingURL=editable_controller.js.map
222
346
  //# sourceMappingURL=editable_controller.js.map
@@ -1,8 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/empty_state_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,7 +16,70 @@ function fillTemplate(template, values) {
19
16
  });
20
17
  }
21
18
 
22
- // src/controllers/empty_state_controller.ts
19
+ var ListenerSet = class {
20
+ #abort = new AbortController();
21
+ add(target, type, handler, options) {
22
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
23
+ }
24
+ dispose() {
25
+ this.#abort.abort();
26
+ this.#abort = new AbortController();
27
+ }
28
+ };
29
+
30
+ var MicrotaskCoalescer = class {
31
+ #run;
32
+ #queued = false;
33
+ #active = false;
34
+ #generation = 0;
35
+ constructor(run) {
36
+ this.#run = run;
37
+ }
38
+ activate() {
39
+ this.#active = true;
40
+ }
41
+ cancel() {
42
+ this.#active = false;
43
+ this.#queued = false;
44
+ this.#generation += 1;
45
+ }
46
+ schedule() {
47
+ if (!this.#active || this.#queued) return;
48
+ this.#queued = true;
49
+ const generation = this.#generation;
50
+ queueMicrotask(() => {
51
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
52
+ this.#queued = false;
53
+ this.#run();
54
+ });
55
+ }
56
+ };
57
+
58
+ var MorphRenderWatcher = class {
59
+ #listeners = new ListenerSet();
60
+ #pass;
61
+ #element = null;
62
+ #onMorph = () => this.#pass.schedule();
63
+ constructor(run) {
64
+ this.#pass = new MicrotaskCoalescer(run);
65
+ }
66
+ observe(element) {
67
+ if (this.#element === element) return;
68
+ this.disconnect();
69
+ this.#element = element;
70
+ this.#pass.activate();
71
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
72
+ }
73
+ schedule() {
74
+ this.#pass.schedule();
75
+ }
76
+ disconnect() {
77
+ this.#listeners.dispose();
78
+ this.#pass.cancel();
79
+ this.#element = null;
80
+ }
81
+ };
82
+
23
83
  var EmptyStateController = class extends Controller {
24
84
  static targets = ["list", "empty"];
25
85
  static values = {
@@ -29,68 +89,40 @@ var EmptyStateController = class extends Controller {
29
89
  };
30
90
  static events = ["change"];
31
91
  #observer = null;
32
- /** Whether the controller is between `connect()` and `disconnect()`. */
33
92
  #connected = false;
34
- /** Last applied empty state; `null` until the first sync so connect emits nothing. */
35
93
  #empty = null;
94
+ #morphRender = new MorphRenderWatcher(() => this.#resync());
36
95
  connect() {
96
+ this.#morphRender.observe(this.element);
37
97
  this.#connected = true;
38
98
  this.#syncObservation();
39
99
  this.#update();
40
100
  }
41
101
  disconnect() {
102
+ this.#morphRender.disconnect();
42
103
  this.#connected = false;
43
104
  this.#stopObserving();
44
105
  }
45
- /** Follows a `list` element swapped in at runtime (Turbo Stream `replace` / morph). */
46
106
  listTargetConnected() {
47
107
  this.#resync();
48
108
  }
49
- /** Releases the observation when the `list` element leaves the target set. */
50
109
  listTargetDisconnected() {
51
110
  this.#resync();
52
111
  }
53
- /** Syncs an `empty` element that arrives — or is replaced — at runtime. */
54
112
  emptyTargetConnected() {
55
113
  this.#resync();
56
114
  }
57
- /**
58
- * Syncs the `empty` element that remains when one leaves the target set. A
59
- * single-target getter resolves to the first `empty` element in document order,
60
- * so a swap that inserts the replacement *before* removing the original (Turbo
61
- * Stream `after` / `before` / `append` followed by `remove`) leaves the
62
- * replacement untouched until the original goes — this callback is that moment.
63
- */
64
115
  emptyTargetDisconnected() {
65
116
  this.#resync();
66
117
  }
67
- /** Re-renders when application code (or a Turbo morph) changes `itemSelector` at runtime. */
68
118
  itemSelectorValueChanged() {
69
119
  this.#resync();
70
120
  }
71
- /**
72
- * Re-points the observation and re-renders after a target or selector change.
73
- * The `#connected` guard is load-bearing: Stimulus runs value and target
74
- * callbacks for the initial markup *before* `connect()` and runs target
75
- * callbacks during teardown *after* `disconnect()`, and re-observing there
76
- * would outlive the controller.
77
- */
78
121
  #resync() {
79
122
  if (!this.#connected) return;
80
123
  this.#syncObservation();
81
124
  this.#update();
82
125
  }
83
- /**
84
- * Points the mutation observation at the current `list` target — re-resolved on
85
- * every sync rather than captured at connect, so an element swapped in at
86
- * runtime is observed instead of the detached original.
87
- *
88
- * The observation covers exactly what the count predicate reads. With no
89
- * `itemSelector` the count is the child element count, which only `childList`
90
- * can change. With one, the predicate reads the children themselves, so
91
- * attribute and descendant mutations are watched too — and the controller's own
92
- * writes are filtered back out, or toggling `hidden` would re-enter the render.
93
- */
94
126
  #syncObservation() {
95
127
  this.#stopObserving();
96
128
  if (!this.hasListTarget || typeof MutationObserver === "undefined") return;
@@ -108,21 +140,11 @@ var EmptyStateController = class extends Controller {
108
140
  this.#observer?.disconnect();
109
141
  this.#observer = null;
110
142
  }
111
- /**
112
- * Whether a mutation can change the item count. An attribute written on a
113
- * target this controller owns is its own echo — `hidden` on `list` / `empty`,
114
- * and the hooks on the controller element when the list *is* that element.
115
- */
116
143
  #affectsCount(record) {
117
144
  if (record.type !== "attributes") return true;
118
145
  const own = record.target === this.listTarget || this.hasEmptyTarget && record.target === this.emptyTarget;
119
146
  return !own;
120
147
  }
121
- /**
122
- * Renders the count, then reports a crossed boundary. The announcement copy is
123
- * read here rather than while rendering: it is the wording of the report, not
124
- * an input to what is displayed.
125
- */
126
148
  #update() {
127
149
  const count = this.#render();
128
150
  if (count === null) return;
@@ -135,12 +157,6 @@ var EmptyStateController = class extends Controller {
135
157
  fillTemplate(empty ? this.announceTextValue : this.announceFilledTextValue, { count })
136
158
  );
137
159
  }
138
- /**
139
- * Syncs visibility and the state hooks to the current item count, and returns
140
- * it. `null` when there is no `list` target to count.
141
- *
142
- * @stimeoRenderRoot
143
- */
144
160
  #render() {
145
161
  if (!this.hasListTarget) return null;
146
162
  const count = this.#count();
@@ -155,7 +171,6 @@ var EmptyStateController = class extends Controller {
155
171
  if (this.hasEmptyTarget) this.emptyTarget.hidden = !empty;
156
172
  return count;
157
173
  }
158
- /** Item count: element children matching `itemSelector`, or all element children. */
159
174
  #count() {
160
175
  const selector = this.itemSelectorValue;
161
176
  if (selector.length === 0) return this.listTarget.childElementCount;
@@ -168,5 +183,4 @@ var EmptyStateController = class extends Controller {
168
183
  };
169
184
 
170
185
  export { EmptyStateController };
171
- //# sourceMappingURL=empty_state_controller.js.map
172
186
  //# sourceMappingURL=empty_state_controller.js.map