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
@@ -8,6 +8,9 @@ function isReservedArrowChord(event, allow = []) {
8
8
  if (!event.key.startsWith("Arrow")) return false;
9
9
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
10
10
  }
11
+ function hasModifierChord(event) {
12
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
13
+ }
11
14
 
12
15
  var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
13
16
  "insertCompositionText",
@@ -15,63 +18,182 @@ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
15
18
  "deleteCompositionText",
16
19
  "deleteByComposition"
17
20
  ]);
21
+ function isCompositionInputType(inputType) {
22
+ return inputType !== void 0 && COMPOSITION_INPUT_TYPES.has(inputType);
23
+ }
24
+ var BEFORE_MORPH_ATTRIBUTE = "turbo:before-morph-attribute";
25
+ var textOf = (element) => element.value;
26
+ var holdsFocus = (element) => element instanceof Node && element.getRootNode().activeElement === element;
18
27
  var CompositionTracker = class {
19
28
  #observedTargets = /* @__PURE__ */ new Set();
20
- #activeTargets = /* @__PURE__ */ new Set();
29
+ #composition = null;
21
30
  #onStart;
22
31
  #onEnd;
32
+ #onAbandon;
23
33
  #confirmedTarget = null;
34
+ #keyComposing = false;
35
+ #connected = false;
36
+ #abandoned = null;
24
37
  constructor(options = {}) {
25
38
  this.#onStart = options.onStart;
26
39
  this.#onEnd = options.onEnd;
40
+ this.#onAbandon = options.onAbandon;
27
41
  }
28
42
  observe(target) {
43
+ this.#connected = true;
29
44
  if (this.#observedTargets.has(target)) return;
30
45
  target.addEventListener("compositionstart", this.#handleStart);
31
46
  target.addEventListener("compositionend", this.#handleEnd);
32
- target.addEventListener("keydown", this.#handleKeydown);
47
+ target.addEventListener("keydown", this.#handleKeydown, true);
48
+ target.addEventListener("beforeinput", this.#handleBeforeInput, true);
49
+ target.addEventListener("input", this.#handleInput, true);
50
+ target.addEventListener("blur", this.#handleBlur, true);
51
+ target.addEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
33
52
  this.#observedTargets.add(target);
34
53
  }
35
54
  unobserve(target) {
36
55
  if (!this.#observedTargets.delete(target)) return;
37
- target.removeEventListener("compositionstart", this.#handleStart);
38
- target.removeEventListener("compositionend", this.#handleEnd);
39
- target.removeEventListener("keydown", this.#handleKeydown);
40
- this.#activeTargets.delete(target);
56
+ this.#release(target);
57
+ if (this.#composition?.target === target) this.#letGo();
41
58
  if (this.#confirmedTarget === target) this.#confirmedTarget = null;
42
59
  }
43
60
  disconnect() {
44
- for (const target of this.#observedTargets) {
45
- target.removeEventListener("compositionstart", this.#handleStart);
46
- target.removeEventListener("compositionend", this.#handleEnd);
47
- target.removeEventListener("keydown", this.#handleKeydown);
48
- }
61
+ this.#connected = false;
62
+ for (const target of this.#observedTargets) this.#release(target);
49
63
  this.#observedTargets.clear();
50
- this.#activeTargets.clear();
64
+ this.#letGo();
51
65
  this.#confirmedTarget = null;
66
+ this.#keyComposing = false;
67
+ this.#abandoned = null;
52
68
  }
53
69
  isComposing(event) {
54
- return this.#activeTargets.size > 0 || event?.isComposing === true;
70
+ this.#settle();
71
+ return event?.isComposing === true || this.#composition !== null;
72
+ }
73
+ composingElement() {
74
+ this.#settle();
75
+ return this.#composition?.element ?? null;
76
+ }
77
+ owns(event) {
78
+ const element = this.composingElement();
79
+ return event.isComposing === true || element !== null && element === event.target;
55
80
  }
56
81
  consumesConfirmedInput(event) {
57
82
  const confirmed = this.#confirmedTarget;
58
83
  this.#confirmedTarget = null;
59
84
  if (confirmed === null || confirmed !== event.target) return false;
60
85
  const inputType = event.inputType;
61
- return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
86
+ return !inputType || isCompositionInputType(inputType);
87
+ }
88
+ #release(target) {
89
+ target.removeEventListener("compositionstart", this.#handleStart);
90
+ target.removeEventListener("compositionend", this.#handleEnd);
91
+ target.removeEventListener("keydown", this.#handleKeydown, true);
92
+ target.removeEventListener("beforeinput", this.#handleBeforeInput, true);
93
+ target.removeEventListener("input", this.#handleInput, true);
94
+ target.removeEventListener("blur", this.#handleBlur, true);
95
+ target.removeEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
96
+ }
97
+ #letGo() {
98
+ const composition = this.#composition;
99
+ if (composition === null) return;
100
+ if (!holdsFocus(composition.element)) {
101
+ this.#composition = null;
102
+ return;
103
+ }
104
+ queueMicrotask(() => {
105
+ if (this.#composition !== composition || this.#observedTargets.has(composition.target)) {
106
+ return;
107
+ }
108
+ this.#composition = null;
109
+ if (!this.#connected) return;
110
+ this.#abandoned = { element: composition.element, inPlace: true };
111
+ this.#runAbandon();
112
+ });
113
+ }
114
+ #settle() {
115
+ const composition = this.#composition;
116
+ if (composition === null) return;
117
+ const { target, element, text } = composition;
118
+ if (target instanceof Node && element instanceof Node && !target.contains(element)) {
119
+ this.#composition = null;
120
+ } else if (textOf(element) !== text) {
121
+ this.#abandon(false);
122
+ }
123
+ }
124
+ #abandon(inPlace) {
125
+ this.#owe(inPlace);
126
+ this.#composition = null;
127
+ queueMicrotask(this.#runAbandon);
128
+ }
129
+ #owe(inPlace) {
130
+ const composition = this.#composition;
131
+ if (composition !== null) this.#abandoned = { element: composition.element, inPlace };
62
132
  }
133
+ #runAbandon = () => {
134
+ const abandoned = this.#abandoned;
135
+ if (abandoned === null) return;
136
+ this.#abandoned = null;
137
+ this.#onAbandon?.(abandoned.element, abandoned.inPlace);
138
+ };
63
139
  #handleStart = (event) => {
64
140
  this.#confirmedTarget = null;
65
- if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
141
+ this.#settle();
142
+ this.#owe(true);
143
+ const target = event.currentTarget;
144
+ const element = event.target;
145
+ this.#composition = { target, element, text: textOf(element) };
146
+ this.#runAbandon();
66
147
  this.#onStart?.(event);
67
148
  };
68
149
  #handleEnd = (event) => {
69
- if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
150
+ this.#runAbandon();
151
+ if (this.#composition?.target === event.currentTarget) this.#composition = null;
70
152
  this.#confirmedTarget = event.target;
71
153
  this.#onEnd?.(event);
72
154
  };
73
- #handleKeydown = () => {
155
+ #handleKeydown = (event) => {
156
+ this.#settle();
157
+ this.#runAbandon();
74
158
  this.#confirmedTarget = null;
159
+ this.#keyComposing = event.isComposing === true;
160
+ };
161
+ #handleBeforeInput = (event) => {
162
+ const keyComposing = this.#keyComposing;
163
+ this.#keyComposing = false;
164
+ this.#settle();
165
+ const { isComposing, inputType } = event;
166
+ if (this.#composition !== null && !keyComposing && !isComposing && inputType && !isCompositionInputType(inputType)) {
167
+ this.#abandon(true);
168
+ }
169
+ this.#runAbandon();
170
+ };
171
+ #handleInput = (event) => {
172
+ const composition = this.#composition;
173
+ const { isTrusted, isComposing, inputType } = event;
174
+ if (composition !== null && event.target === composition.element && (isTrusted || isComposing || isCompositionInputType(inputType))) {
175
+ composition.text = textOf(composition.element);
176
+ } else {
177
+ this.#settle();
178
+ }
179
+ this.#runAbandon();
180
+ };
181
+ #handleBlur = (event) => {
182
+ this.#settle();
183
+ this.#runAbandon();
184
+ const composition = this.#composition;
185
+ if (composition === null || composition.target === composition.element) return;
186
+ const { element } = composition;
187
+ if (!(element instanceof Node) || event.target !== element) return;
188
+ const root = element.getRootNode();
189
+ if (root.activeElement !== element) this.#composition = null;
190
+ };
191
+ #handleBeforeMorphAttribute = (event) => {
192
+ this.#settle();
193
+ const composition = this.#composition;
194
+ if (composition?.target !== event.currentTarget || event.target !== composition.element) return;
195
+ const detail = event.detail;
196
+ if (detail?.attributeName === "value") event.preventDefault();
75
197
  };
76
198
  };
77
199
 
@@ -117,6 +239,9 @@ var MicrotaskCoalescer = class {
117
239
  this.#queued = false;
118
240
  this.#generation += 1;
119
241
  }
242
+ get pending() {
243
+ return this.#queued;
244
+ }
120
245
  schedule() {
121
246
  if (!this.#active || this.#queued) return;
122
247
  this.#queued = true;
@@ -147,6 +272,9 @@ var MorphRenderWatcher = class {
147
272
  schedule() {
148
273
  this.#pass.schedule();
149
274
  }
275
+ get pending() {
276
+ return this.#pass.pending;
277
+ }
150
278
  disconnect() {
151
279
  this.#listeners.dispose();
152
280
  this.#pass.cancel();
@@ -191,11 +319,12 @@ var ToolbarController = class extends Controller {
191
319
  #observer = null;
192
320
  #connected = false;
193
321
  #morph = new MorphRenderWatcher(() => this.#ensureTabStop());
322
+ #listeners = new ListenerSet();
194
323
  connect() {
195
324
  this.#morph.observe(this.element);
196
325
  this.#ensureTabStop();
197
- this.element.addEventListener("keydown", this.#onKeydown);
198
- this.element.addEventListener("focusin", this.#onFocusin);
326
+ this.#listeners.add(this.element, "keydown", this.#onKeydown);
327
+ this.#listeners.add(this.element, "focusin", this.#onFocusin);
199
328
  this.#composition.observe(this.element);
200
329
  if (typeof MutationObserver !== "undefined") {
201
330
  const observer = new MutationObserver(() => this.#morph.schedule());
@@ -215,8 +344,7 @@ var ToolbarController = class extends Controller {
215
344
  disconnect() {
216
345
  this.#morph.disconnect();
217
346
  this.#connected = false;
218
- this.element.removeEventListener("keydown", this.#onKeydown);
219
- this.element.removeEventListener("focusin", this.#onFocusin);
347
+ this.#listeners.dispose();
220
348
  this.#composition.disconnect();
221
349
  this.#observer?.disconnect();
222
350
  this.#observer = null;
@@ -245,6 +373,7 @@ var ToolbarController = class extends Controller {
245
373
  #handleKeydown(event) {
246
374
  if (event.defaultPrevented) return;
247
375
  if (isReservedArrowChord(event)) return;
376
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
248
377
  if (this.#composition.isComposing(event)) return;
249
378
  this.#ensureTabStop();
250
379
  const from = this.#indexOf(event.target);
@@ -30,6 +30,107 @@ 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
+ }
36
+
37
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
38
+ if (element.getAttribute(name) === value) return false;
39
+ beforeWrite?.();
40
+ if (value === null) element.removeAttribute(name);
41
+ else element.setAttribute(name, value);
42
+ return true;
43
+ }
44
+
45
+ var AuthoredMutationWatcher = class {
46
+ #onAuthored;
47
+ #claims = null;
48
+ #observer = null;
49
+ constructor(onAuthored) {
50
+ this.#onAuthored = onAuthored;
51
+ }
52
+ observe(root, options) {
53
+ this.disconnect();
54
+ const observer = new MutationObserver((records) => {
55
+ if (this.#observer !== observer) return;
56
+ const authored = this.#filter(records);
57
+ if (authored.length > 0) this.#onAuthored(authored);
58
+ });
59
+ this.#observer = observer;
60
+ this.#claims = { attributes: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map() };
61
+ observer.observe(root, {
62
+ attributes: true,
63
+ attributeFilter: [...options.attributes],
64
+ subtree: options.subtree ?? false,
65
+ childList: options.childList ?? false
66
+ });
67
+ if (options.ancestorAttributes) {
68
+ let ancestor = root.parentElement;
69
+ while (ancestor) {
70
+ observer.observe(ancestor, {
71
+ attributes: true,
72
+ attributeFilter: [...options.ancestorAttributes]
73
+ });
74
+ ancestor = ancestor.parentElement;
75
+ }
76
+ }
77
+ }
78
+ claim(target, name, value) {
79
+ const claims = this.#claims?.attributes;
80
+ let attributes = claims?.get(target);
81
+ if (!attributes) {
82
+ attributes = /* @__PURE__ */ new Map();
83
+ claims?.set(target, attributes);
84
+ }
85
+ const pending = attributes.get(name);
86
+ attributes.set(name, { value, count: (pending?.count ?? 0) + 1 });
87
+ }
88
+ claimCurrent(target, name) {
89
+ this.claim(target, name, target.getAttribute(name));
90
+ }
91
+ claimChildList(target, added, removed) {
92
+ const claims = this.#claims?.children;
93
+ const pending = claims?.get(target) ?? { added: /* @__PURE__ */ new Map(), removed: /* @__PURE__ */ new Map() };
94
+ for (const node of added) pending.added.set(node, (pending.added.get(node) ?? 0) + 1);
95
+ for (const node of removed) pending.removed.set(node, (pending.removed.get(node) ?? 0) + 1);
96
+ claims?.set(target, pending);
97
+ }
98
+ take() {
99
+ return this.#filter(this.#observer?.takeRecords() ?? []);
100
+ }
101
+ disconnect() {
102
+ this.#observer?.disconnect();
103
+ this.#observer = null;
104
+ this.#claims = null;
105
+ }
106
+ #filter(records) {
107
+ const authored = records.filter((record) => !this.#consume(record));
108
+ this.#claims?.attributes.clear();
109
+ this.#claims?.children.clear();
110
+ return authored;
111
+ }
112
+ #consume(record) {
113
+ if (record.type === "attributes") {
114
+ const target = record.target;
115
+ const pending2 = this.#claims?.attributes.get(target)?.get(record.attributeName);
116
+ if (!pending2 || pending2.count === 0 || target.getAttribute(record.attributeName) !== pending2.value) {
117
+ return false;
118
+ }
119
+ pending2.count -= 1;
120
+ return true;
121
+ }
122
+ const pending = this.#claims?.children.get(record.target);
123
+ if (!pending) return false;
124
+ const added = [...record.addedNodes];
125
+ const removed = [...record.removedNodes];
126
+ if (added.some((node) => !pending.added.get(node)) || removed.some((node) => !pending.removed.get(node))) {
127
+ return false;
128
+ }
129
+ for (const node of added) pending.added.set(node, (pending.added.get(node) ?? 0) - 1);
130
+ for (const node of removed) pending.removed.set(node, (pending.removed.get(node) ?? 0) - 1);
131
+ return true;
132
+ }
133
+ };
33
134
 
34
135
  function writeField(field, value) {
35
136
  if (field.value === value) return false;
@@ -111,6 +212,9 @@ var MicrotaskCoalescer = class {
111
212
  this.#queued = false;
112
213
  this.#generation += 1;
113
214
  }
215
+ get pending() {
216
+ return this.#queued;
217
+ }
114
218
  schedule() {
115
219
  if (!this.#active || this.#queued) return;
116
220
  this.#queued = true;
@@ -141,6 +245,9 @@ var MorphRenderWatcher = class {
141
245
  schedule() {
142
246
  this.#pass.schedule();
143
247
  }
248
+ get pending() {
249
+ return this.#pass.pending;
250
+ }
144
251
  disconnect() {
145
252
  this.#listeners.dispose();
146
253
  this.#pass.cancel();
@@ -262,19 +369,19 @@ function findTypeaheadMatch(items, from, query, label = (item) => typeaheadLabel
262
369
  }
263
370
 
264
371
  var OBSERVED_ATTRIBUTES = ["aria-selected", "data-value"];
265
- function setAttributeIfChanged(element, name, value) {
266
- if (element.getAttribute(name) !== value) element.setAttribute(name, value);
267
- }
268
372
  var NESTED_INTERACTIVE = INTERACTIVE_HOST_SELECTOR;
269
373
  var TreeViewController = class extends Controller {
270
374
  #moves = new MoveCounter();
375
+ #connectionGeneration = 0;
271
376
  static targets = ["item", "group", "field"];
272
377
  static actions = ["onKeydown", "select", "toggle"];
273
378
  static events = ["select", "toggle", "reconcile"];
274
379
  #roving = new RovingTabindex(() => this.itemTargets);
275
380
  #typeahead = new Typeahead();
276
381
  #reconcile = new MorphRenderWatcher(() => this.#reconcileSelection());
277
- #observer = null;
382
+ #observer = new AuthoredMutationWatcher((records) => {
383
+ if (this.#concernsItems(records)) this.#reconcile.schedule();
384
+ });
278
385
  #settled = { item: null, value: "" };
279
386
  #connected = false;
280
387
  #order = [];
@@ -293,9 +400,10 @@ var TreeViewController = class extends Controller {
293
400
  if (this.#focused === row && row.isConnected) this.#focused = null;
294
401
  });
295
402
  };
403
+ #listeners = new ListenerSet();
296
404
  connect() {
297
- this.element.addEventListener("focusin", this.#onFocusIn);
298
- this.element.addEventListener("focusout", this.#onFocusOut);
405
+ this.#listeners.add(this.element, "focusin", this.#onFocusIn);
406
+ this.#listeners.add(this.element, "focusout", this.#onFocusOut);
299
407
  this.#seedFocused();
300
408
  this.#reconcileExpansion();
301
409
  this.#normalizeSelection();
@@ -314,12 +422,11 @@ var TreeViewController = class extends Controller {
314
422
  this.#reconcile.schedule();
315
423
  }
316
424
  disconnect() {
317
- this.element.removeEventListener("focusin", this.#onFocusIn);
318
- this.element.removeEventListener("focusout", this.#onFocusOut);
425
+ this.#connectionGeneration += 1;
426
+ this.#listeners.dispose();
319
427
  this.#connected = false;
320
428
  this.#reconcile.disconnect();
321
- this.#observer?.disconnect();
322
- this.#observer = null;
429
+ this.#observer.disconnect();
323
430
  this.#typeahead.reset();
324
431
  this.#order = [];
325
432
  this.#focused = null;
@@ -337,7 +444,7 @@ var TreeViewController = class extends Controller {
337
444
  const items = this.itemTargets;
338
445
  const selected = items.find((item) => item.getAttribute("aria-selected") === "true");
339
446
  for (const item of items) {
340
- setAttributeIfChanged(item, "aria-selected", item === selected ? "true" : "false");
447
+ this.#writeSelection(item, item === selected ? "true" : "false");
341
448
  }
342
449
  }
343
450
  #mirrorField(notify) {
@@ -353,7 +460,7 @@ var TreeViewController = class extends Controller {
353
460
  this.#reconcileExpansion();
354
461
  this.#normalizeSelection();
355
462
  this.#normalizeTabStop();
356
- this.#dropOwnRecords();
463
+ this.#flushMutations();
357
464
  this.#mirrorField(false);
358
465
  this.#reportMove();
359
466
  }
@@ -364,22 +471,17 @@ var TreeViewController = class extends Controller {
364
471
  this.dispatch("reconcile", { detail: { item: selection.item } });
365
472
  }
366
473
  #observeItems() {
367
- const observer = new MutationObserver((records) => {
368
- if (this.#concernsItems(records)) this.#reconcile.schedule();
369
- });
370
- observer.observe(this.element, {
474
+ this.#observer.observe(this.element, {
371
475
  subtree: true,
372
- attributes: true,
373
- attributeFilter: OBSERVED_ATTRIBUTES
476
+ attributes: OBSERVED_ATTRIBUTES
374
477
  });
375
- this.#observer = observer;
376
478
  }
377
479
  #concernsItems(records) {
378
480
  const items = new Set(this.itemTargets);
379
481
  return records.some((record) => items.has(record.target));
380
482
  }
381
- #dropOwnRecords() {
382
- this.#observer?.takeRecords();
483
+ #flushMutations() {
484
+ if (this.#concernsItems(this.#observer.take())) this.#reconcile.schedule();
383
485
  }
384
486
  itemTargetDisconnected(item) {
385
487
  if (!this.#connected || !this.element.isConnected) return;
@@ -408,6 +510,7 @@ var TreeViewController = class extends Controller {
408
510
  onKeydown(event) {
409
511
  if (event.defaultPrevented) return;
410
512
  if (isReservedArrowChord(event)) return;
513
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
411
514
  if (event.isComposing) return;
412
515
  const item = this.#ownerItem(event);
413
516
  if (!item) return;
@@ -553,10 +656,11 @@ var TreeViewController = class extends Controller {
553
656
  }
554
657
  #select(item, reason = "user") {
555
658
  if (this.#isDisabled(item)) return;
659
+ const generation = this.#connectionGeneration;
556
660
  for (const candidate of this.itemTargets) {
557
- setAttributeIfChanged(candidate, "aria-selected", candidate === item ? "true" : "false");
661
+ this.#writeSelection(candidate, candidate === item ? "true" : "false");
558
662
  }
559
- this.#dropOwnRecords();
663
+ this.#flushMutations();
560
664
  const selection = this.#selection();
561
665
  const changed = selection.item !== this.#settled.item || selection.value !== this.#settled.value;
562
666
  this.#settled = selection;
@@ -566,7 +670,7 @@ var TreeViewController = class extends Controller {
566
670
  }
567
671
  const token = this.#moves.record();
568
672
  this.#mirrorField(true);
569
- if (!this.#moves.isLatest(token)) return;
673
+ if (generation !== this.#connectionGeneration || !this.#moves.isLatest(token)) return;
570
674
  this.dispatch("select", { detail: { item, reason } });
571
675
  }
572
676
  #setExpanded(item, group, expanded, reason = "user") {
@@ -631,7 +735,7 @@ var TreeViewController = class extends Controller {
631
735
  }
632
736
  const selected = visible.find((item) => item.getAttribute("aria-selected") === "true");
633
737
  const next = selected ?? visible[0];
634
- this.#roving.setActive(items.findIndex((item) => item === next));
738
+ this.#roving.setActive(items.indexOf(next));
635
739
  }
636
740
  get #visibleItems() {
637
741
  return this.itemTargets.filter((item) => {
@@ -664,6 +768,11 @@ var TreeViewController = class extends Controller {
664
768
  return text;
665
769
  });
666
770
  }
771
+ #writeSelection(target, value) {
772
+ setAttributeIfChanged(target, "aria-selected", value, () => {
773
+ this.#observer.claim(target, "aria-selected", value);
774
+ });
775
+ }
667
776
  };
668
777
 
669
778
  export { TreeViewController };