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,87 +1,354 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/editable_controller.ts
3
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
4
+ if (element.getAttribute(name) === value) return false;
5
+ if (value === null) element.removeAttribute(name);
6
+ else element.setAttribute(name, value);
7
+ return true;
8
+ }
9
+
10
+ function sharedRegistry(key, create) {
11
+ const symbol = Symbol.for(key);
12
+ const scope = globalThis;
13
+ const existing = scope[symbol];
14
+ if (existing !== void 0) return existing;
15
+ const registry2 = create();
16
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
17
+ return registry2;
18
+ }
19
+
20
+ var registry = sharedRegistry(
21
+ "stimeo-ui.attribute-lease.registry.v1",
22
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
23
+ );
24
+ function isAttributeValue(value) {
25
+ return value === null || typeof value === "string";
26
+ }
27
+ function readLeaseRecord(element, record) {
28
+ try {
29
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
30
+ if (!Array.isArray(parsed)) return void 0;
31
+ const [original, written, ...rest] = parsed;
32
+ const linked = rest[0] !== false;
33
+ const beneath = linked ? rest : rest.slice(1);
34
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
35
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
36
+ } catch {
37
+ return void 0;
38
+ }
39
+ }
40
+ function attributeStack(element, attribute) {
41
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
42
+ registry.stacks.set(element, byAttribute);
43
+ const known = byAttribute.get(attribute);
44
+ if (known) return known;
45
+ const found = [];
46
+ for (const record of registry.names.get(attribute) ?? []) {
47
+ const read = readLeaseRecord(element, record);
48
+ if (read) found.push({ ...read, record });
49
+ }
50
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
51
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
52
+ lease: null,
53
+ record,
54
+ written,
55
+ original,
56
+ linked: linked && index > 0
57
+ }));
58
+ byAttribute.set(attribute, stack);
59
+ return stack;
60
+ }
61
+ function findHold(stack, lease, record) {
62
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
63
+ }
64
+ var AttributeLease = class {
65
+ #attribute;
66
+ #record;
67
+ #base;
68
+ #preserveAuthor;
69
+ #held = /* @__PURE__ */ new Set();
70
+ constructor(attribute, owner, options = {}) {
71
+ this.#attribute = attribute;
72
+ this.#record = `data-${owner}-${attribute}-lease`;
73
+ this.#base = options.base === true;
74
+ this.#preserveAuthor = options.preserveAuthor === true;
75
+ const names = registry.names;
76
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
77
+ }
78
+ write(element, value) {
79
+ const stack = attributeStack(element, this.#attribute);
80
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
81
+ this.#held.add(element);
82
+ const shows = !this.#covered(element, stack, hold);
83
+ hold.written = value;
84
+ this.#mark(element, stack);
85
+ if (shows) this.#reflect(element, value);
86
+ }
87
+ return(element) {
88
+ const held = this.#held.delete(element);
89
+ const known = registry.stacks.get(element)?.has(this.#attribute);
90
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
91
+ const hold = this.#take(stack);
92
+ if (!hold) return false;
93
+ const carries = element.getAttribute(this.#attribute) === hold.written;
94
+ const owns = carries && !this.#covered(element, stack, hold);
95
+ const index = stack.indexOf(hold);
96
+ stack.splice(index, 1);
97
+ const above = stack[index];
98
+ if (above) above.linked &&= hold.linked;
99
+ this.#mark(element, stack);
100
+ if (!owns) return true;
101
+ const below = hold.linked ? stack[index - 1] : void 0;
102
+ this.#reflect(element, below ? below.written : hold.original);
103
+ return true;
104
+ }
105
+ returnAll() {
106
+ for (const element of Array.from(this.#held)) this.return(element);
107
+ }
108
+ #take(stack) {
109
+ const hold = findHold(stack, this, this.#record);
110
+ if (hold) hold.lease = this;
111
+ return hold;
112
+ }
113
+ #acquire(element, stack) {
114
+ const current = element.getAttribute(this.#attribute);
115
+ const top = stack.at(-1);
116
+ const bottom = stack[0];
117
+ const over = top?.written === current ? top : void 0;
118
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
119
+ const hold = {
120
+ lease: this,
121
+ record: this.#record,
122
+ written: current,
123
+ original: from ? from.original : current,
124
+ linked: !this.#base && over !== void 0
125
+ };
126
+ if (this.#base && bottom) {
127
+ bottom.linked = true;
128
+ stack.unshift(hold);
129
+ } else stack.push(hold);
130
+ return hold;
131
+ }
132
+ #covered(element, stack, hold) {
133
+ const current = element.getAttribute(this.#attribute);
134
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
135
+ }
136
+ #mark(element, stack) {
137
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
138
+ stack.forEach((hold, index) => {
139
+ const names = stack.slice(0, index).map((under) => under.record);
140
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
141
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
142
+ records.set(
143
+ hold.record,
144
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
145
+ );
146
+ });
147
+ for (const [name, recorded] of records) {
148
+ setAttributeIfChanged(element, name, recorded);
149
+ }
150
+ }
151
+ #reflect(element, value) {
152
+ setAttributeIfChanged(element, this.#attribute, value);
153
+ }
154
+ };
4
155
 
5
- // src/utils/composition_tracker.ts
6
156
  var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
7
157
  "insertCompositionText",
8
158
  "insertFromComposition",
9
159
  "deleteCompositionText",
10
160
  "deleteByComposition"
11
161
  ]);
162
+ function isCompositionInputType(inputType) {
163
+ return inputType !== void 0 && COMPOSITION_INPUT_TYPES.has(inputType);
164
+ }
165
+ var BEFORE_MORPH_ATTRIBUTE = "turbo:before-morph-attribute";
166
+ var textOf = (element) => element.value;
167
+ var holdsFocus = (element) => element instanceof Node && element.getRootNode().activeElement === element;
12
168
  var CompositionTracker = class {
13
169
  #observedTargets = /* @__PURE__ */ new Set();
14
- #activeTargets = /* @__PURE__ */ new Set();
170
+ #composition = null;
15
171
  #onStart;
16
172
  #onEnd;
17
- /** The field whose confirming `input` is still owed, while the window is open. */
173
+ #onAbandon;
18
174
  #confirmedTarget = null;
175
+ #keyComposing = false;
176
+ #connected = false;
177
+ #abandoned = null;
19
178
  constructor(options = {}) {
20
179
  this.#onStart = options.onStart;
21
180
  this.#onEnd = options.onEnd;
181
+ this.#onAbandon = options.onAbandon;
22
182
  }
23
- /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
24
183
  observe(target) {
184
+ this.#connected = true;
25
185
  if (this.#observedTargets.has(target)) return;
26
186
  target.addEventListener("compositionstart", this.#handleStart);
27
187
  target.addEventListener("compositionend", this.#handleEnd);
28
- target.addEventListener("keydown", this.#handleKeydown);
188
+ target.addEventListener("keydown", this.#handleKeydown, true);
189
+ target.addEventListener("beforeinput", this.#handleBeforeInput, true);
190
+ target.addEventListener("input", this.#handleInput, true);
191
+ target.addEventListener("blur", this.#handleBlur, true);
192
+ target.addEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
29
193
  this.#observedTargets.add(target);
30
194
  }
31
- /** Stops tracking one target and clears any active composition it owned. */
32
195
  unobserve(target) {
33
196
  if (!this.#observedTargets.delete(target)) return;
34
- target.removeEventListener("compositionstart", this.#handleStart);
35
- target.removeEventListener("compositionend", this.#handleEnd);
36
- target.removeEventListener("keydown", this.#handleKeydown);
37
- this.#activeTargets.delete(target);
197
+ this.#release(target);
198
+ if (this.#composition?.target === target) this.#letGo();
38
199
  if (this.#confirmedTarget === target) this.#confirmedTarget = null;
39
200
  }
40
- /** Releases every listener and clears state so reconnect starts cleanly. */
41
201
  disconnect() {
42
- for (const target of this.#observedTargets) {
43
- target.removeEventListener("compositionstart", this.#handleStart);
44
- target.removeEventListener("compositionend", this.#handleEnd);
45
- target.removeEventListener("keydown", this.#handleKeydown);
46
- }
202
+ this.#connected = false;
203
+ for (const target of this.#observedTargets) this.#release(target);
47
204
  this.#observedTargets.clear();
48
- this.#activeTargets.clear();
205
+ this.#letGo();
49
206
  this.#confirmedTarget = null;
207
+ this.#keyComposing = false;
208
+ this.#abandoned = null;
50
209
  }
51
- /** True when lifecycle tracking or the current event reports composition. */
52
210
  isComposing(event) {
53
- return this.#activeTargets.size > 0 || event?.isComposing === true;
54
- }
55
- /**
56
- * Whether `event` is the `input` echoing the composition just confirmed.
57
- *
58
- * Asking closes the window either way, so one confirmation is folded at most
59
- * once and a consumer asks once per `input`.
60
- */
211
+ this.#settle();
212
+ return event?.isComposing === true || this.#composition !== null;
213
+ }
214
+ composingElement() {
215
+ this.#settle();
216
+ return this.#composition?.element ?? null;
217
+ }
218
+ owns(event) {
219
+ const element = this.composingElement();
220
+ return event.isComposing === true || element !== null && element === event.target;
221
+ }
61
222
  consumesConfirmedInput(event) {
62
223
  const confirmed = this.#confirmedTarget;
63
224
  this.#confirmedTarget = null;
64
225
  if (confirmed === null || confirmed !== event.target) return false;
65
226
  const inputType = event.inputType;
66
- return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
227
+ return !inputType || isCompositionInputType(inputType);
67
228
  }
229
+ #release(target) {
230
+ target.removeEventListener("compositionstart", this.#handleStart);
231
+ target.removeEventListener("compositionend", this.#handleEnd);
232
+ target.removeEventListener("keydown", this.#handleKeydown, true);
233
+ target.removeEventListener("beforeinput", this.#handleBeforeInput, true);
234
+ target.removeEventListener("input", this.#handleInput, true);
235
+ target.removeEventListener("blur", this.#handleBlur, true);
236
+ target.removeEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
237
+ }
238
+ #letGo() {
239
+ const composition = this.#composition;
240
+ if (composition === null) return;
241
+ if (!holdsFocus(composition.element)) {
242
+ this.#composition = null;
243
+ return;
244
+ }
245
+ queueMicrotask(() => {
246
+ if (this.#composition !== composition || this.#observedTargets.has(composition.target)) {
247
+ return;
248
+ }
249
+ this.#composition = null;
250
+ if (!this.#connected) return;
251
+ this.#abandoned = { element: composition.element, inPlace: true };
252
+ this.#runAbandon();
253
+ });
254
+ }
255
+ #settle() {
256
+ const composition = this.#composition;
257
+ if (composition === null) return;
258
+ const { target, element, text } = composition;
259
+ if (target instanceof Node && element instanceof Node && !target.contains(element)) {
260
+ this.#composition = null;
261
+ } else if (textOf(element) !== text) {
262
+ this.#abandon(false);
263
+ }
264
+ }
265
+ #abandon(inPlace) {
266
+ this.#owe(inPlace);
267
+ this.#composition = null;
268
+ queueMicrotask(this.#runAbandon);
269
+ }
270
+ #owe(inPlace) {
271
+ const composition = this.#composition;
272
+ if (composition !== null) this.#abandoned = { element: composition.element, inPlace };
273
+ }
274
+ #runAbandon = () => {
275
+ const abandoned = this.#abandoned;
276
+ if (abandoned === null) return;
277
+ this.#abandoned = null;
278
+ this.#onAbandon?.(abandoned.element, abandoned.inPlace);
279
+ };
68
280
  #handleStart = (event) => {
69
281
  this.#confirmedTarget = null;
70
- if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
282
+ this.#settle();
283
+ this.#owe(true);
284
+ const target = event.currentTarget;
285
+ const element = event.target;
286
+ this.#composition = { target, element, text: textOf(element) };
287
+ this.#runAbandon();
71
288
  this.#onStart?.(event);
72
289
  };
73
290
  #handleEnd = (event) => {
74
- if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
291
+ this.#runAbandon();
292
+ if (this.#composition?.target === event.currentTarget) this.#composition = null;
75
293
  this.#confirmedTarget = event.target;
76
294
  this.#onEnd?.(event);
77
295
  };
78
- /** A key on an observed field opens an edit of its own, so no echo is owed. */
79
- #handleKeydown = () => {
296
+ #handleKeydown = (event) => {
297
+ this.#settle();
298
+ this.#runAbandon();
80
299
  this.#confirmedTarget = null;
300
+ this.#keyComposing = event.isComposing === true;
301
+ };
302
+ #handleBeforeInput = (event) => {
303
+ const keyComposing = this.#keyComposing;
304
+ this.#keyComposing = false;
305
+ this.#settle();
306
+ const { isComposing, inputType } = event;
307
+ if (this.#composition !== null && !keyComposing && !isComposing && inputType && !isCompositionInputType(inputType)) {
308
+ this.#abandon(true);
309
+ }
310
+ this.#runAbandon();
311
+ };
312
+ #handleInput = (event) => {
313
+ const composition = this.#composition;
314
+ const { isTrusted, isComposing, inputType } = event;
315
+ if (composition !== null && event.target === composition.element && (isTrusted || isComposing || isCompositionInputType(inputType))) {
316
+ composition.text = textOf(composition.element);
317
+ } else {
318
+ this.#settle();
319
+ }
320
+ this.#runAbandon();
321
+ };
322
+ #handleBlur = (event) => {
323
+ this.#settle();
324
+ this.#runAbandon();
325
+ const composition = this.#composition;
326
+ if (composition === null || composition.target === composition.element) return;
327
+ const { element } = composition;
328
+ if (!(element instanceof Node) || event.target !== element) return;
329
+ const root = element.getRootNode();
330
+ if (root.activeElement !== element) this.#composition = null;
331
+ };
332
+ #handleBeforeMorphAttribute = (event) => {
333
+ this.#settle();
334
+ const composition = this.#composition;
335
+ if (composition?.target !== event.currentTarget || event.target !== composition.element) return;
336
+ const detail = event.detail;
337
+ if (detail?.attributeName === "value") event.preventDefault();
81
338
  };
82
339
  };
83
340
 
84
- // src/controllers/editable_controller.ts
341
+ var ListenerSet = class {
342
+ #abort = new AbortController();
343
+ add(target, type, handler, options) {
344
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
345
+ }
346
+ dispose() {
347
+ this.#abort.abort();
348
+ this.#abort = new AbortController();
349
+ }
350
+ };
351
+
85
352
  var EditableController = class extends Controller {
86
353
  static targets = ["display", "input"];
87
354
  static values = {
@@ -89,31 +356,11 @@ var EditableController = class extends Controller {
89
356
  };
90
357
  static actions = ["cancel", "edit", "onDisplayKeydown", "onKeydown", "revert", "save"];
91
358
  static events = ["cancel", "change"];
92
- /** The value captured when edit mode began, used to detect real changes. */
93
359
  #previousValue = "";
94
- /**
95
- * The value the last save replaced, or `null` when there is nothing to undo.
96
- * Cleared on connect: a value from before a page restore is unrecoverable, so
97
- * `revert()` must not resurrect one.
98
- */
99
360
  #revertValue = null;
100
- /**
101
- * Owns IME lifecycle state for the edit surface, so a keydown that belongs to
102
- * a composition (cancel or confirm) is never treated as an edit command.
103
- */
104
361
  #composition = new CompositionTracker();
105
- /**
106
- * Watches focus leaving the editor from wherever it currently sits.
107
- *
108
- * `focusout` bubbles where `blur` does not, so one listener on the root sees
109
- * every departure — including one from a Save or Cancel button the consumer
110
- * placed beside the input. Binding the input alone would make the promise
111
- * "saves wherever focus moved" true only for focus that leaves the input
112
- * itself, and tabbing straight past an inner button would strand the editor
113
- * open.
114
- *
115
- * @stimeoRuntimeOnly `submitOnBlur` decides whether this one blur commits the edit.
116
- */
362
+ #hidden = new AttributeLease("hidden", this.identifier);
363
+ #connected = false;
117
364
  #onFocusOut = (event) => {
118
365
  const from = event.target;
119
366
  if (this.hasDisplayTarget && from instanceof Node && this.displayTarget.contains(from)) return;
@@ -121,32 +368,35 @@ var EditableController = class extends Controller {
121
368
  if (next instanceof Node && this.element.contains(next)) return;
122
369
  if (this.submitOnBlurValue) this.#commit(false);
123
370
  };
124
- /** Establishes the initial display mode (display shown, input hidden). */
371
+ #listeners = new ListenerSet();
125
372
  connect() {
373
+ this.#connected = true;
126
374
  if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
127
- this.element.addEventListener("focusout", this.#onFocusOut);
375
+ this.#listeners.add(this.element, "focusout", this.#onFocusOut);
128
376
  this.#revertValue = null;
129
377
  this.#setMode("display");
130
378
  }
131
- /** Releases the composition and focus listeners so nothing outlives the element. */
132
379
  disconnect() {
380
+ this.#connected = false;
133
381
  this.#composition.disconnect();
134
- this.element.removeEventListener("focusout", this.#onFocusOut);
382
+ this.#listeners.dispose();
135
383
  }
136
- /** Tracks an input added initially or after connect (e.g. a Turbo swap). */
137
384
  inputTargetConnected(input) {
138
385
  this.#composition.observe(input);
139
386
  this.#applyMode();
140
387
  }
141
- /** Removes composition listeners when the active input is replaced or removed. */
142
388
  inputTargetDisconnected(input) {
143
389
  this.#composition.unobserve(input);
390
+ if (!this.inputTargets.includes(input)) this.#hidden.return(input);
391
+ if (this.#connected) this.#applyMode();
144
392
  }
145
- /** Re-hides or re-shows a display element that arrived after the mode was set. */
146
393
  displayTargetConnected() {
147
394
  this.#applyMode();
148
395
  }
149
- /** Enters edit mode: seeds the input from the declared value, focuses, selects. */
396
+ displayTargetDisconnected(display) {
397
+ if (!this.displayTargets.includes(display)) this.#hidden.return(display);
398
+ if (this.#connected) this.#applyMode();
399
+ }
150
400
  edit() {
151
401
  if (this.#isEditing || !this.hasInputTarget || !this.hasDisplayTarget) return;
152
402
  this.#previousValue = this.#currentValue;
@@ -155,35 +405,26 @@ var EditableController = class extends Controller {
155
405
  this.inputTarget.focus();
156
406
  this.inputTarget.select();
157
407
  }
158
- /** Commits the edit and returns focus to the display element. */
159
408
  save() {
160
409
  this.#commit(true);
161
410
  }
162
- /** Discards edits, returns to display mode, and dispatches `cancel`. */
163
411
  cancel() {
164
412
  if (!this.#isEditing) return;
165
413
  this.#setMode("display");
166
414
  if (this.hasDisplayTarget) this.displayTarget.focus();
167
415
  this.dispatch("cancel", { detail: {} });
168
416
  }
169
- /**
170
- * Puts back the value the last save replaced — for a consumer whose server
171
- * rejected it. Silent by design: a `change` here would re-enter the same
172
- * handler that asked for the undo. One save, one undo.
173
- */
174
417
  revert() {
175
418
  if (this.#revertValue === null || this.#isEditing) return;
176
419
  this.#writeValue(this.#revertValue);
177
420
  this.#revertValue = null;
178
421
  }
179
- /** Adds `F2` as an editing entry point alongside the button's native activation. */
180
422
  onDisplayKeydown(event) {
181
423
  if (event.key === "F2") {
182
424
  event.preventDefault();
183
425
  this.edit();
184
426
  }
185
427
  }
186
- /** Commits on `Enter` (or `Ctrl+Enter` when multiline) and cancels on `Escape`. */
187
428
  onKeydown(event) {
188
429
  if (this.#composition.isComposing(event)) return;
189
430
  if (event.key === "Escape") {
@@ -199,13 +440,6 @@ var EditableController = class extends Controller {
199
440
  this.#commit(true);
200
441
  }
201
442
  }
202
- /**
203
- * Returns to display mode, storing the input's value and dispatching `change`
204
- * when it differs from where editing began.
205
- *
206
- * @param restoreFocus - Move focus back to the display element (explicit
207
- * commit) rather than honoring the user's new focus target (blur).
208
- */
209
443
  #commit(restoreFocus) {
210
444
  if (!this.#isEditing || !this.hasInputTarget) return;
211
445
  const value = this.inputTarget.value.trim();
@@ -218,39 +452,32 @@ var EditableController = class extends Controller {
218
452
  this.dispatch("change", { detail: { value, previous } });
219
453
  }
220
454
  }
221
- /** Stores `value` wherever the display element declares it. */
222
455
  #writeValue(value) {
223
456
  if (!this.hasDisplayTarget) return;
224
457
  const display = this.displayTarget;
225
458
  if (display.hasAttribute("data-value")) display.dataset.value = value;
226
459
  else display.textContent = value;
227
460
  }
228
- /** Derives both elements' visibility from the mode currently in the DOM. */
229
461
  #applyMode() {
230
462
  const editing = this.#isEditing;
231
- if (this.hasDisplayTarget) this.displayTarget.hidden = editing;
232
- if (this.hasInputTarget) this.inputTarget.hidden = !editing;
463
+ if (this.hasDisplayTarget) this.#hidden.write(this.displayTarget, editing ? "" : null);
464
+ if (this.hasInputTarget) this.#hidden.write(this.inputTarget, editing ? null : "");
233
465
  }
234
- /** Records the mode, then brings both elements in line with it. */
235
466
  #setMode(mode) {
236
467
  this.element.dataset.mode = mode;
237
468
  this.#applyMode();
238
469
  }
239
- /** The value the display element declares, trimmed. */
240
470
  get #currentValue() {
241
471
  const display = this.displayTarget;
242
472
  return (display.dataset.value ?? display.textContent ?? "").trim();
243
473
  }
244
- /** Whether the editing control is a multi-line `<textarea>`. */
245
474
  get #isMultiline() {
246
475
  return this.inputTarget.tagName === "TEXTAREA";
247
476
  }
248
- /** Whether the controller is currently in edit mode. */
249
477
  get #isEditing() {
250
478
  return this.element.dataset.mode === "editing";
251
479
  }
252
480
  };
253
481
 
254
482
  export { EditableController };
255
- //# sourceMappingURL=editable_controller.js.map
256
483
  //# sourceMappingURL=editable_controller.js.map