stimeo-ui 0.17.0 → 0.18.1

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +136 -2
  3. data/README.md +19 -10
  4. data/dist/cable/index.js +22 -14
  5. data/dist/controllers/accordion_controller.js +14 -6
  6. data/dist/controllers/alert_dialog_controller.js +145 -118
  7. data/dist/controllers/announcer_controller.js +113 -17
  8. data/dist/controllers/auto_submit_controller.js +185 -60
  9. data/dist/controllers/avatar_controller.js +55 -43
  10. data/dist/controllers/bulk_select_controller.js +64 -50
  11. data/dist/controllers/calendar_controller.js +55 -53
  12. data/dist/controllers/carousel_controller.js +124 -86
  13. data/dist/controllers/character_counter_controller.js +291 -95
  14. data/dist/controllers/checkbox_controller.js +6 -0
  15. data/dist/controllers/clipboard_controller.js +118 -27
  16. data/dist/controllers/collapsible_controller.js +52 -49
  17. data/dist/controllers/color_picker_controller.js +29 -7
  18. data/dist/controllers/combobox_controller.js +210 -71
  19. data/dist/controllers/command_palette_controller.js +290 -140
  20. data/dist/controllers/conditional_fields_controller.js +145 -58
  21. data/dist/controllers/confirm_controller.js +145 -118
  22. data/dist/controllers/context_menu_controller.js +138 -73
  23. data/dist/controllers/countdown_controller.js +109 -21
  24. data/dist/controllers/currency_input_controller.js +446 -72
  25. data/dist/controllers/data_grid_controller.js +134 -28
  26. data/dist/controllers/date_range_picker_controller.js +93 -72
  27. data/dist/controllers/dialog_controller.js +145 -118
  28. data/dist/controllers/direct_upload_controller.js +24 -14
  29. data/dist/controllers/dirty_form_controller.js +68 -31
  30. data/dist/controllers/dismissible_controller.js +66 -46
  31. data/dist/controllers/drawer_controller.js +145 -118
  32. data/dist/controllers/dropdown_controller.js +76 -63
  33. data/dist/controllers/editable_controller.js +200 -62
  34. data/dist/controllers/empty_state_controller.js +13 -6
  35. data/dist/controllers/file_dropzone_controller.js +55 -48
  36. data/dist/controllers/filter_controller.js +9 -2
  37. data/dist/controllers/flash_controller.js +66 -33
  38. data/dist/controllers/focus_controller.js +39 -14
  39. data/dist/controllers/form_field_controller.js +52 -43
  40. data/dist/controllers/form_validation_controller.js +18 -11
  41. data/dist/controllers/frame_loading_controller.js +55 -48
  42. data/dist/controllers/hover_card_controller.js +67 -61
  43. data/dist/controllers/idle_controller.js +10 -6
  44. data/dist/controllers/input_mask_controller.js +434 -24
  45. data/dist/controllers/intersection_controller.js +3 -0
  46. data/dist/controllers/lazy_frame_controller.js +15 -3
  47. data/dist/controllers/listbox_controller.js +260 -79
  48. data/dist/controllers/local_time_controller.js +48 -13
  49. data/dist/controllers/masonry_controller.js +11 -4
  50. data/dist/controllers/menu_controller.js +138 -82
  51. data/dist/controllers/menubar_controller.js +65 -19
  52. data/dist/controllers/meter_controller.js +270 -24
  53. data/dist/controllers/multi_select_controller.js +272 -95
  54. data/dist/controllers/navigation_menu_controller.js +67 -19
  55. data/dist/controllers/nested_form_controller.js +98 -64
  56. data/dist/controllers/network_status_controller.js +70 -52
  57. data/dist/controllers/number_input_controller.js +344 -94
  58. data/dist/controllers/optimistic_controller.js +29 -11
  59. data/dist/controllers/otp_controller.js +502 -104
  60. data/dist/controllers/overflow_indicator_controller.js +6 -0
  61. data/dist/controllers/overflow_menu_controller.js +136 -21
  62. data/dist/controllers/pagination_controller.js +47 -14
  63. data/dist/controllers/password_reveal_controller.js +52 -49
  64. data/dist/controllers/password_strength_controller.js +13 -6
  65. data/dist/controllers/persist_controller.js +48 -128
  66. data/dist/controllers/pointer_drag_controller.js +255 -24
  67. data/dist/controllers/popover_controller.js +91 -65
  68. data/dist/controllers/portal_controller.js +6 -0
  69. data/dist/controllers/preview_guard_controller.js +14 -3
  70. data/dist/controllers/progress_controller.js +271 -27
  71. data/dist/controllers/radio_group_controller.js +167 -80
  72. data/dist/controllers/range_slider_controller.js +27 -7
  73. data/dist/controllers/rating_controller.js +93 -57
  74. data/dist/controllers/read_more_controller.js +10 -6
  75. data/dist/controllers/reading_progress_controller.js +22 -5
  76. data/dist/controllers/relative_time_controller.js +75 -11
  77. data/dist/controllers/reset_on_restore_controller.js +6 -0
  78. data/dist/controllers/resizable_controller.js +55 -22
  79. data/dist/controllers/roving_controller.js +10 -4
  80. data/dist/controllers/scroll_area_controller.js +129 -88
  81. data/dist/controllers/scroll_restore_controller.js +14 -2
  82. data/dist/controllers/scroll_visibility_controller.js +93 -60
  83. data/dist/controllers/scrollspy_controller.js +11 -5
  84. data/dist/controllers/separator_controller.js +59 -43
  85. data/dist/controllers/sidebar_controller.js +150 -122
  86. data/dist/controllers/skeleton_controller.js +55 -48
  87. data/dist/controllers/slider_controller.js +27 -6
  88. data/dist/controllers/smart_sticky_header_controller.js +13 -5
  89. data/dist/controllers/sortable_controller.js +27 -14
  90. data/dist/controllers/spinner_controller.js +55 -48
  91. data/dist/controllers/step_indicator_controller.js +6 -0
  92. data/dist/controllers/stepper_controller.js +6 -0
  93. data/dist/controllers/stick_to_bottom_controller.js +19 -8
  94. data/dist/controllers/sticky_observer_controller.js +49 -43
  95. data/dist/controllers/submit_once_controller.js +145 -85
  96. data/dist/controllers/switch_controller.js +172 -28
  97. data/dist/controllers/tabs_controller.js +6 -1
  98. data/dist/controllers/tags_input_controller.js +157 -28
  99. data/dist/controllers/textarea_autosize_controller.js +40 -19
  100. data/dist/controllers/theme_controller.js +10 -4
  101. data/dist/controllers/time_picker_controller.js +192 -54
  102. data/dist/controllers/toast_controller.js +58 -27
  103. data/dist/controllers/toggle_group_controller.js +158 -56
  104. data/dist/controllers/toolbar_controller.js +150 -21
  105. data/dist/controllers/tree_view_controller.js +134 -25
  106. data/dist/index.js +2515 -1315
  107. data/dist/positioning/index.js +6 -0
  108. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  109. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  110. data/lib/stimeo/ui/version.rb +1 -1
  111. metadata +2 -2
@@ -1,5 +1,103 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
4
+ if (element.getAttribute(name) === value) return false;
5
+ beforeWrite?.();
6
+ if (value === null) element.removeAttribute(name);
7
+ else element.setAttribute(name, value);
8
+ return true;
9
+ }
10
+
11
+ var AuthoredMutationWatcher = class {
12
+ #onAuthored;
13
+ #claims = null;
14
+ #observer = null;
15
+ constructor(onAuthored) {
16
+ this.#onAuthored = onAuthored;
17
+ }
18
+ observe(root, options) {
19
+ this.disconnect();
20
+ const observer = new MutationObserver((records) => {
21
+ if (this.#observer !== observer) return;
22
+ const authored = this.#filter(records);
23
+ if (authored.length > 0) this.#onAuthored(authored);
24
+ });
25
+ this.#observer = observer;
26
+ this.#claims = { attributes: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map() };
27
+ observer.observe(root, {
28
+ attributes: true,
29
+ attributeFilter: [...options.attributes],
30
+ subtree: options.subtree ?? false,
31
+ childList: options.childList ?? false
32
+ });
33
+ if (options.ancestorAttributes) {
34
+ let ancestor = root.parentElement;
35
+ while (ancestor) {
36
+ observer.observe(ancestor, {
37
+ attributes: true,
38
+ attributeFilter: [...options.ancestorAttributes]
39
+ });
40
+ ancestor = ancestor.parentElement;
41
+ }
42
+ }
43
+ }
44
+ claim(target, name, value) {
45
+ const claims = this.#claims?.attributes;
46
+ let attributes = claims?.get(target);
47
+ if (!attributes) {
48
+ attributes = /* @__PURE__ */ new Map();
49
+ claims?.set(target, attributes);
50
+ }
51
+ const pending = attributes.get(name);
52
+ attributes.set(name, { value, count: (pending?.count ?? 0) + 1 });
53
+ }
54
+ claimCurrent(target, name) {
55
+ this.claim(target, name, target.getAttribute(name));
56
+ }
57
+ claimChildList(target, added, removed) {
58
+ const claims = this.#claims?.children;
59
+ const pending = claims?.get(target) ?? { added: /* @__PURE__ */ new Map(), removed: /* @__PURE__ */ new Map() };
60
+ for (const node of added) pending.added.set(node, (pending.added.get(node) ?? 0) + 1);
61
+ for (const node of removed) pending.removed.set(node, (pending.removed.get(node) ?? 0) + 1);
62
+ claims?.set(target, pending);
63
+ }
64
+ take() {
65
+ return this.#filter(this.#observer?.takeRecords() ?? []);
66
+ }
67
+ disconnect() {
68
+ this.#observer?.disconnect();
69
+ this.#observer = null;
70
+ this.#claims = null;
71
+ }
72
+ #filter(records) {
73
+ const authored = records.filter((record) => !this.#consume(record));
74
+ this.#claims?.attributes.clear();
75
+ this.#claims?.children.clear();
76
+ return authored;
77
+ }
78
+ #consume(record) {
79
+ if (record.type === "attributes") {
80
+ const target = record.target;
81
+ const pending2 = this.#claims?.attributes.get(target)?.get(record.attributeName);
82
+ if (!pending2 || pending2.count === 0 || target.getAttribute(record.attributeName) !== pending2.value) {
83
+ return false;
84
+ }
85
+ pending2.count -= 1;
86
+ return true;
87
+ }
88
+ const pending = this.#claims?.children.get(record.target);
89
+ if (!pending) return false;
90
+ const added = [...record.addedNodes];
91
+ const removed = [...record.removedNodes];
92
+ if (added.some((node) => !pending.added.get(node)) || removed.some((node) => !pending.removed.get(node))) {
93
+ return false;
94
+ }
95
+ for (const node of added) pending.added.set(node, (pending.added.get(node) ?? 0) - 1);
96
+ for (const node of removed) pending.removed.set(node, (pending.removed.get(node) ?? 0) - 1);
97
+ return true;
98
+ }
99
+ };
100
+
3
101
  function setDefaultAttribute(element, name, value) {
4
102
  if (element.hasAttribute(name)) return false;
5
103
  element.setAttribute(name, value);
@@ -41,6 +139,17 @@ function isInteractiveHost(element) {
41
139
  return false;
42
140
  }
43
141
 
142
+ var ListenerSet = class {
143
+ #abort = new AbortController();
144
+ add(target, type, handler, options) {
145
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
146
+ }
147
+ dispose() {
148
+ this.#abort.abort();
149
+ this.#abort = new AbortController();
150
+ }
151
+ };
152
+
44
153
  var MicrotaskCoalescer = class {
45
154
  #run;
46
155
  #queued = false;
@@ -57,6 +166,9 @@ var MicrotaskCoalescer = class {
57
166
  this.#queued = false;
58
167
  this.#generation += 1;
59
168
  }
169
+ get pending() {
170
+ return this.#queued;
171
+ }
60
172
  schedule() {
61
173
  if (!this.#active || this.#queued) return;
62
174
  this.#queued = true;
@@ -69,6 +181,34 @@ var MicrotaskCoalescer = class {
69
181
  }
70
182
  };
71
183
 
184
+ var MorphRenderWatcher = class {
185
+ #listeners = new ListenerSet();
186
+ #pass;
187
+ #element = null;
188
+ #onMorph = () => this.#pass.schedule();
189
+ constructor(run) {
190
+ this.#pass = new MicrotaskCoalescer(run);
191
+ }
192
+ observe(element) {
193
+ if (this.#element === element) return;
194
+ this.disconnect();
195
+ this.#element = element;
196
+ this.#pass.activate();
197
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
198
+ }
199
+ schedule() {
200
+ this.#pass.schedule();
201
+ }
202
+ get pending() {
203
+ return this.#pass.pending;
204
+ }
205
+ disconnect() {
206
+ this.#listeners.dispose();
207
+ this.#pass.cancel();
208
+ this.#element = null;
209
+ }
210
+ };
211
+
72
212
  var MoveCounter = class {
73
213
  #sequence = 0;
74
214
  record() {
@@ -92,14 +232,14 @@ var OBSERVED_ATTRIBUTES = [
92
232
  var OBSERVED_ANCESTOR_ATTRIBUTES = ["contenteditable"];
93
233
  var SwitchController = class extends Controller {
94
234
  #moves = new MoveCounter();
235
+ #connectionGeneration = 0;
95
236
  static targets = ["field"];
96
237
  static actions = ["onKeydown", "toggle"];
97
238
  static events = ["change", "reconcile"];
98
239
  #ownedDefaults = /* @__PURE__ */ new Map();
99
- #internalAttributeValues = /* @__PURE__ */ new Map();
100
- #pass = new MicrotaskCoalescer(() => this.#reconcile());
240
+ #pass = new MorphRenderWatcher(() => this.#reconcile());
101
241
  #committedChecked = false;
102
- #attributeObserver = new MutationObserver((records) => {
242
+ #attributeObserver = new AuthoredMutationWatcher((records) => {
103
243
  this.#releaseAuthoredDefaults(records);
104
244
  this.#pass.schedule();
105
245
  });
@@ -108,13 +248,14 @@ var SwitchController = class extends Controller {
108
248
  event.preventDefault();
109
249
  event.stopImmediatePropagation();
110
250
  };
251
+ #listeners = new ListenerSet();
111
252
  connect() {
112
253
  this.#releaseDefaultsRewrittenWhileAway();
113
254
  this.#reconcileDefaults();
114
255
  this.#mirrorField(false);
115
256
  this.#committedChecked = this.#checked;
116
- this.element.addEventListener("click", this.#onClickCapture, true);
117
- this.#pass.activate();
257
+ this.#listeners.add(this.element, "click", this.#onClickCapture, { capture: true });
258
+ this.#pass.observe(this.element);
118
259
  this.#observeAttributes();
119
260
  }
120
261
  fieldTargetConnected() {
@@ -124,10 +265,10 @@ var SwitchController = class extends Controller {
124
265
  this.#pass.schedule();
125
266
  }
126
267
  disconnect() {
127
- this.#pass.cancel();
128
- this.element.removeEventListener("click", this.#onClickCapture, true);
268
+ this.#connectionGeneration += 1;
269
+ this.#pass.disconnect();
270
+ this.#listeners.dispose();
129
271
  this.#attributeObserver.disconnect();
130
- this.#internalAttributeValues.clear();
131
272
  }
132
273
  toggle() {
133
274
  if (!this.#isSupportedHost || this.#isActivationDisabled) return;
@@ -151,11 +292,12 @@ var SwitchController = class extends Controller {
151
292
  this.toggle();
152
293
  }
153
294
  #reconcile() {
154
- this.#releaseAuthoredDefaults(this.#attributeObserver.takeRecords());
155
- this.#attributeObserver.disconnect();
295
+ this.#releaseAuthoredDefaults(this.#attributeObserver.take());
156
296
  this.#reconcileDefaults();
157
297
  this.#mirrorField(false);
158
- this.#observeAttributes();
298
+ const authored = this.#attributeObserver.take();
299
+ this.#releaseAuthoredDefaults(authored);
300
+ if (authored.length > 0) this.#pass.schedule();
159
301
  this.#reportMove();
160
302
  }
161
303
  #reportMove() {
@@ -181,7 +323,10 @@ var SwitchController = class extends Controller {
181
323
  if (moved && notify) commitField(this.fieldTarget);
182
324
  }
183
325
  #setOwnedDefault(name, value) {
184
- if (setDefaultAttribute(this.element, name, value)) this.#ownedDefaults.set(name, value);
326
+ if (setDefaultAttribute(this.element, name, value)) {
327
+ this.#ownedDefaults.set(name, value);
328
+ this.#attributeObserver.claimCurrent(this.element, name);
329
+ }
185
330
  }
186
331
  #releaseDefaultsRewrittenWhileAway() {
187
332
  for (const [name, written] of this.#ownedDefaults) {
@@ -195,31 +340,22 @@ var SwitchController = class extends Controller {
195
340
  const name = record.attributeName;
196
341
  if (name && this.#ownedDefaults.has(name)) {
197
342
  const value = this.element.getAttribute(name);
198
- if (value !== null && this.#internalAttributeValues.get(name) !== value) {
343
+ if (value !== null) {
199
344
  this.#ownedDefaults.delete(name);
200
345
  }
201
346
  }
202
347
  }
203
348
  }
204
- this.#internalAttributeValues.clear();
205
349
  }
206
350
  #removeOwnedDefaults() {
207
- for (const name of this.#ownedDefaults.keys()) this.element.removeAttribute(name);
351
+ for (const name of this.#ownedDefaults.keys()) this.#writeAttribute(name, null);
208
352
  this.#ownedDefaults.clear();
209
353
  }
210
354
  #observeAttributes() {
211
355
  this.#attributeObserver.observe(this.element, {
212
- attributes: true,
213
- attributeFilter: OBSERVED_ATTRIBUTES
356
+ attributes: OBSERVED_ATTRIBUTES,
357
+ ancestorAttributes: OBSERVED_ANCESTOR_ATTRIBUTES
214
358
  });
215
- let ancestor = this.element.parentElement;
216
- while (ancestor) {
217
- this.#attributeObserver.observe(ancestor, {
218
- attributes: true,
219
- attributeFilter: OBSERVED_ANCESTOR_ATTRIBUTES
220
- });
221
- ancestor = ancestor.parentElement;
222
- }
223
359
  }
224
360
  get #isSupportedHost() {
225
361
  if (this.element instanceof HTMLButtonElement) return this.element.type === "button";
@@ -238,10 +374,13 @@ var SwitchController = class extends Controller {
238
374
  return this.element.getAttribute("aria-checked") === "true";
239
375
  }
240
376
  #commit(value) {
377
+ const generation = this.#connectionGeneration;
241
378
  const reflected = value ? "true" : "false";
242
- this.#internalAttributeValues.set("aria-checked", reflected);
379
+ const authored = this.#attributeObserver.take();
380
+ this.#releaseAuthoredDefaults(authored);
381
+ if (authored.length > 0) this.#pass.schedule();
243
382
  if (this.#ownedDefaults.has("aria-checked")) this.#ownedDefaults.set("aria-checked", reflected);
244
- this.element.setAttribute("aria-checked", reflected);
383
+ this.#writeAttribute("aria-checked", reflected);
245
384
  const changed = value !== this.#committedChecked;
246
385
  this.#committedChecked = value;
247
386
  if (!changed) {
@@ -250,9 +389,14 @@ var SwitchController = class extends Controller {
250
389
  }
251
390
  const token = this.#moves.record();
252
391
  this.#mirrorField(true);
253
- if (!this.#moves.isLatest(token)) return;
392
+ if (generation !== this.#connectionGeneration || !this.#moves.isLatest(token)) return;
254
393
  this.dispatch("change", { detail: { checked: value } });
255
394
  }
395
+ #writeAttribute(name, value) {
396
+ setAttributeIfChanged(this.element, name, value, () => {
397
+ this.#attributeObserver.claim(this.element, name, value);
398
+ });
399
+ }
256
400
  };
257
401
 
258
402
  export { SwitchController };
@@ -30,6 +30,9 @@ function isReservedArrowChord(event, allow = []) {
30
30
  if (!event.key.startsWith("Arrow")) return false;
31
31
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
32
32
  }
33
+ function hasModifierChord(event) {
34
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
35
+ }
33
36
 
34
37
  function targetSelector(identifier, name) {
35
38
  return `[data-${identifier}-target~="${name}"]`;
@@ -62,11 +65,13 @@ var TabsController = class extends Controller {
62
65
  onKeydown(event) {
63
66
  if (event.defaultPrevented) return;
64
67
  if (isReservedArrowChord(event)) return;
68
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
65
69
  const tabs = this.tabTargets;
66
70
  const currentIndex = tabs.indexOf(event.currentTarget);
67
71
  if (currentIndex === -1) return;
68
72
  let nextIndex = null;
69
- const step = isRtl(this.element) ? -1 : 1;
73
+ const directionElement = this.hasListTarget ? this.listTarget : this.element;
74
+ const step = isRtl(directionElement) ? -1 : 1;
70
75
  switch (event.key) {
71
76
  case "ArrowRight":
72
77
  nextIndex = (currentIndex + step + tabs.length) % tabs.length;
@@ -1,5 +1,12 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ function fillTemplate(template, values) {
4
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
5
+ const replacement = Object.hasOwn(values, name) ? values[name] : void 0;
6
+ return replacement === void 0 ? match : String(replacement);
7
+ });
8
+ }
9
+
3
10
  function announce(message, options = {}) {
4
11
  const text = message.trim();
5
12
  if (text.length === 0) return;
@@ -9,12 +16,6 @@ function announce(message, options = {}) {
9
16
  })
10
17
  );
11
18
  }
12
- function fillTemplate(template, values) {
13
- return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
14
- const replacement = values[name];
15
- return replacement === void 0 ? match : String(replacement);
16
- });
17
- }
18
19
 
19
20
  function isRtl(element) {
20
21
  return window.getComputedStyle(element).direction === "rtl";
@@ -67,7 +68,7 @@ var RovingTabindex = class {
67
68
  };
68
69
 
69
70
  var ChipRow = class {
70
- #directionElement;
71
+ #getDirectionElement;
71
72
  #getItems;
72
73
  #getButton;
73
74
  #onRemove;
@@ -75,7 +76,7 @@ var ChipRow = class {
75
76
  #roving = new RovingTabindex(() => this.buttons);
76
77
  #container = null;
77
78
  constructor(options) {
78
- this.#directionElement = options.directionElement;
79
+ this.#getDirectionElement = options.getDirectionElement;
79
80
  this.#getItems = options.getItems;
80
81
  this.#getButton = options.getButton ?? ((item) => matchingPart(item, "button"));
81
82
  this.#onRemove = options.onRemove;
@@ -156,7 +157,7 @@ var ChipRow = class {
156
157
  if (!entry) return;
157
158
  const buttons = entry.buttons;
158
159
  const index = entry.buttonIndex;
159
- switch (logicalArrowKey(event.key, this.#directionElement)) {
160
+ switch (logicalArrowKey(event.key, this.#getDirectionElement())) {
160
161
  case "ArrowLeft":
161
162
  if (index > 0) {
162
163
  event.preventDefault();
@@ -186,63 +187,182 @@ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
186
187
  "deleteCompositionText",
187
188
  "deleteByComposition"
188
189
  ]);
190
+ function isCompositionInputType(inputType) {
191
+ return inputType !== void 0 && COMPOSITION_INPUT_TYPES.has(inputType);
192
+ }
193
+ var BEFORE_MORPH_ATTRIBUTE = "turbo:before-morph-attribute";
194
+ var textOf = (element) => element.value;
195
+ var holdsFocus = (element) => element instanceof Node && element.getRootNode().activeElement === element;
189
196
  var CompositionTracker = class {
190
197
  #observedTargets = /* @__PURE__ */ new Set();
191
- #activeTargets = /* @__PURE__ */ new Set();
198
+ #composition = null;
192
199
  #onStart;
193
200
  #onEnd;
201
+ #onAbandon;
194
202
  #confirmedTarget = null;
203
+ #keyComposing = false;
204
+ #connected = false;
205
+ #abandoned = null;
195
206
  constructor(options = {}) {
196
207
  this.#onStart = options.onStart;
197
208
  this.#onEnd = options.onEnd;
209
+ this.#onAbandon = options.onAbandon;
198
210
  }
199
211
  observe(target) {
212
+ this.#connected = true;
200
213
  if (this.#observedTargets.has(target)) return;
201
214
  target.addEventListener("compositionstart", this.#handleStart);
202
215
  target.addEventListener("compositionend", this.#handleEnd);
203
- target.addEventListener("keydown", this.#handleKeydown);
216
+ target.addEventListener("keydown", this.#handleKeydown, true);
217
+ target.addEventListener("beforeinput", this.#handleBeforeInput, true);
218
+ target.addEventListener("input", this.#handleInput, true);
219
+ target.addEventListener("blur", this.#handleBlur, true);
220
+ target.addEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
204
221
  this.#observedTargets.add(target);
205
222
  }
206
223
  unobserve(target) {
207
224
  if (!this.#observedTargets.delete(target)) return;
208
- target.removeEventListener("compositionstart", this.#handleStart);
209
- target.removeEventListener("compositionend", this.#handleEnd);
210
- target.removeEventListener("keydown", this.#handleKeydown);
211
- this.#activeTargets.delete(target);
225
+ this.#release(target);
226
+ if (this.#composition?.target === target) this.#letGo();
212
227
  if (this.#confirmedTarget === target) this.#confirmedTarget = null;
213
228
  }
214
229
  disconnect() {
215
- for (const target of this.#observedTargets) {
216
- target.removeEventListener("compositionstart", this.#handleStart);
217
- target.removeEventListener("compositionend", this.#handleEnd);
218
- target.removeEventListener("keydown", this.#handleKeydown);
219
- }
230
+ this.#connected = false;
231
+ for (const target of this.#observedTargets) this.#release(target);
220
232
  this.#observedTargets.clear();
221
- this.#activeTargets.clear();
233
+ this.#letGo();
222
234
  this.#confirmedTarget = null;
235
+ this.#keyComposing = false;
236
+ this.#abandoned = null;
223
237
  }
224
238
  isComposing(event) {
225
- return this.#activeTargets.size > 0 || event?.isComposing === true;
239
+ this.#settle();
240
+ return event?.isComposing === true || this.#composition !== null;
241
+ }
242
+ composingElement() {
243
+ this.#settle();
244
+ return this.#composition?.element ?? null;
245
+ }
246
+ owns(event) {
247
+ const element = this.composingElement();
248
+ return event.isComposing === true || element !== null && element === event.target;
226
249
  }
227
250
  consumesConfirmedInput(event) {
228
251
  const confirmed = this.#confirmedTarget;
229
252
  this.#confirmedTarget = null;
230
253
  if (confirmed === null || confirmed !== event.target) return false;
231
254
  const inputType = event.inputType;
232
- return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
255
+ return !inputType || isCompositionInputType(inputType);
233
256
  }
257
+ #release(target) {
258
+ target.removeEventListener("compositionstart", this.#handleStart);
259
+ target.removeEventListener("compositionend", this.#handleEnd);
260
+ target.removeEventListener("keydown", this.#handleKeydown, true);
261
+ target.removeEventListener("beforeinput", this.#handleBeforeInput, true);
262
+ target.removeEventListener("input", this.#handleInput, true);
263
+ target.removeEventListener("blur", this.#handleBlur, true);
264
+ target.removeEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
265
+ }
266
+ #letGo() {
267
+ const composition = this.#composition;
268
+ if (composition === null) return;
269
+ if (!holdsFocus(composition.element)) {
270
+ this.#composition = null;
271
+ return;
272
+ }
273
+ queueMicrotask(() => {
274
+ if (this.#composition !== composition || this.#observedTargets.has(composition.target)) {
275
+ return;
276
+ }
277
+ this.#composition = null;
278
+ if (!this.#connected) return;
279
+ this.#abandoned = { element: composition.element, inPlace: true };
280
+ this.#runAbandon();
281
+ });
282
+ }
283
+ #settle() {
284
+ const composition = this.#composition;
285
+ if (composition === null) return;
286
+ const { target, element, text } = composition;
287
+ if (target instanceof Node && element instanceof Node && !target.contains(element)) {
288
+ this.#composition = null;
289
+ } else if (textOf(element) !== text) {
290
+ this.#abandon(false);
291
+ }
292
+ }
293
+ #abandon(inPlace) {
294
+ this.#owe(inPlace);
295
+ this.#composition = null;
296
+ queueMicrotask(this.#runAbandon);
297
+ }
298
+ #owe(inPlace) {
299
+ const composition = this.#composition;
300
+ if (composition !== null) this.#abandoned = { element: composition.element, inPlace };
301
+ }
302
+ #runAbandon = () => {
303
+ const abandoned = this.#abandoned;
304
+ if (abandoned === null) return;
305
+ this.#abandoned = null;
306
+ this.#onAbandon?.(abandoned.element, abandoned.inPlace);
307
+ };
234
308
  #handleStart = (event) => {
235
309
  this.#confirmedTarget = null;
236
- if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
310
+ this.#settle();
311
+ this.#owe(true);
312
+ const target = event.currentTarget;
313
+ const element = event.target;
314
+ this.#composition = { target, element, text: textOf(element) };
315
+ this.#runAbandon();
237
316
  this.#onStart?.(event);
238
317
  };
239
318
  #handleEnd = (event) => {
240
- if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
319
+ this.#runAbandon();
320
+ if (this.#composition?.target === event.currentTarget) this.#composition = null;
241
321
  this.#confirmedTarget = event.target;
242
322
  this.#onEnd?.(event);
243
323
  };
244
- #handleKeydown = () => {
324
+ #handleKeydown = (event) => {
325
+ this.#settle();
326
+ this.#runAbandon();
245
327
  this.#confirmedTarget = null;
328
+ this.#keyComposing = event.isComposing === true;
329
+ };
330
+ #handleBeforeInput = (event) => {
331
+ const keyComposing = this.#keyComposing;
332
+ this.#keyComposing = false;
333
+ this.#settle();
334
+ const { isComposing, inputType } = event;
335
+ if (this.#composition !== null && !keyComposing && !isComposing && inputType && !isCompositionInputType(inputType)) {
336
+ this.#abandon(true);
337
+ }
338
+ this.#runAbandon();
339
+ };
340
+ #handleInput = (event) => {
341
+ const composition = this.#composition;
342
+ const { isTrusted, isComposing, inputType } = event;
343
+ if (composition !== null && event.target === composition.element && (isTrusted || isComposing || isCompositionInputType(inputType))) {
344
+ composition.text = textOf(composition.element);
345
+ } else {
346
+ this.#settle();
347
+ }
348
+ this.#runAbandon();
349
+ };
350
+ #handleBlur = (event) => {
351
+ this.#settle();
352
+ this.#runAbandon();
353
+ const composition = this.#composition;
354
+ if (composition === null || composition.target === composition.element) return;
355
+ const { element } = composition;
356
+ if (!(element instanceof Node) || event.target !== element) return;
357
+ const root = element.getRootNode();
358
+ if (root.activeElement !== element) this.#composition = null;
359
+ };
360
+ #handleBeforeMorphAttribute = (event) => {
361
+ this.#settle();
362
+ const composition = this.#composition;
363
+ if (composition?.target !== event.currentTarget || event.target !== composition.element) return;
364
+ const detail = event.detail;
365
+ if (detail?.attributeName === "value") event.preventDefault();
246
366
  };
247
367
  };
248
368
 
@@ -295,6 +415,9 @@ var MicrotaskCoalescer = class {
295
415
  this.#queued = false;
296
416
  this.#generation += 1;
297
417
  }
418
+ get pending() {
419
+ return this.#queued;
420
+ }
298
421
  schedule() {
299
422
  if (!this.#active || this.#queued) return;
300
423
  this.#queued = true;
@@ -325,6 +448,9 @@ var MorphRenderWatcher = class {
325
448
  schedule() {
326
449
  this.#pass.schedule();
327
450
  }
451
+ get pending() {
452
+ return this.#pass.pending;
453
+ }
328
454
  disconnect() {
329
455
  this.#listeners.dispose();
330
456
  this.#pass.cancel();
@@ -481,7 +607,7 @@ var TagsInputController = class _TagsInputController extends Controller {
481
607
  });
482
608
  #reconcile = new MorphRenderWatcher(() => this.#reconcileTags());
483
609
  #chipRow = new ChipRow({
484
- directionElement: this.element,
610
+ getDirectionElement: () => this.#chipDirectionElement,
485
611
  getItems: () => this.tagTargets,
486
612
  getButton: (tag) => matchingPart(tag, `button${this.#rows.selector("remove")}`),
487
613
  onRemove: (index) => this.#removeAt(index),
@@ -554,7 +680,7 @@ var TagsInputController = class _TagsInputController extends Controller {
554
680
  }
555
681
  return;
556
682
  }
557
- if (logicalArrowKey(event.key, this.element) === "ArrowLeft" && this.inputTarget.value === "") {
683
+ if (logicalArrowKey(event.key, this.#chipDirectionElement) === "ArrowLeft" && this.inputTarget.value === "") {
558
684
  if (this.#chipRow.length > 0) {
559
685
  event.preventDefault();
560
686
  this.#chipRow.focusLast();
@@ -607,6 +733,9 @@ var TagsInputController = class _TagsInputController extends Controller {
607
733
  }
608
734
  if (!this.#chipRow.focusAfterRemoval(index)) this.#focusInput();
609
735
  }
736
+ get #chipDirectionElement() {
737
+ return this.hasTagsTarget ? this.tagsTarget : this.element;
738
+ }
610
739
  #focusInput() {
611
740
  if (this.hasInputTarget) this.inputTarget.focus();
612
741
  }