stimeo-ui 0.17.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -1
  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 +289 -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 +64 -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 +199 -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 +411 -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 +251 -94
  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 +249 -23
  67. data/dist/controllers/popover_controller.js +82 -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 +2451 -1312
  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
@@ -5,9 +5,9 @@ function sharedRegistry(key, create) {
5
5
  const scope = globalThis;
6
6
  const existing = scope[symbol];
7
7
  if (existing !== void 0) return existing;
8
- const registry = create();
9
- Object.defineProperty(globalThis, symbol, { value: registry });
10
- return registry;
8
+ const registry2 = create();
9
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
10
+ return registry2;
11
11
  }
12
12
 
13
13
  var ids = sharedRegistry("stimeo-ui.aria-ids.registry.v1", () => ({ counter: 0 }));
@@ -26,6 +26,17 @@ function ensureId(element, prefix = "stimeo") {
26
26
  return id;
27
27
  }
28
28
 
29
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
30
+ if (element.getAttribute(name) === value) return false;
31
+ if (value === null) element.removeAttribute(name);
32
+ else element.setAttribute(name, value);
33
+ return true;
34
+ }
35
+
36
+ var registry = sharedRegistry(
37
+ "stimeo-ui.attribute-lease.registry.v1",
38
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
39
+ );
29
40
  function isAttributeValue(value) {
30
41
  return value === null || typeof value === "string";
31
42
  }
@@ -42,24 +53,46 @@ function readLeaseRecord(element, record) {
42
53
  return void 0;
43
54
  }
44
55
  }
45
- var AttributeLease = class _AttributeLease {
46
- static #registry = sharedRegistry(
47
- "stimeo-ui.attribute-lease.registry.v1",
48
- () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
49
- );
56
+ function attributeStack(element, attribute) {
57
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
58
+ registry.stacks.set(element, byAttribute);
59
+ const known = byAttribute.get(attribute);
60
+ if (known) return known;
61
+ const found = [];
62
+ for (const record of registry.names.get(attribute) ?? []) {
63
+ const read = readLeaseRecord(element, record);
64
+ if (read) found.push({ ...read, record });
65
+ }
66
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
67
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
68
+ lease: null,
69
+ record,
70
+ written,
71
+ original,
72
+ linked: linked && index > 0
73
+ }));
74
+ byAttribute.set(attribute, stack);
75
+ return stack;
76
+ }
77
+ function findHold(stack, lease, record) {
78
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
79
+ }
80
+ var AttributeLease = class {
50
81
  #attribute;
51
82
  #record;
52
83
  #base;
84
+ #preserveAuthor;
53
85
  #held = /* @__PURE__ */ new Set();
54
86
  constructor(attribute, owner, options = {}) {
55
87
  this.#attribute = attribute;
56
88
  this.#record = `data-${owner}-${attribute}-lease`;
57
89
  this.#base = options.base === true;
58
- const names = _AttributeLease.#registry.names;
90
+ this.#preserveAuthor = options.preserveAuthor === true;
91
+ const names = registry.names;
59
92
  names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
60
93
  }
61
94
  write(element, value) {
62
- const stack = this.#stack(element);
95
+ const stack = attributeStack(element, this.#attribute);
63
96
  const hold = this.#take(stack) ?? this.#acquire(element, stack);
64
97
  this.#held.add(element);
65
98
  const shows = !this.#covered(element, stack, hold);
@@ -69,8 +102,8 @@ var AttributeLease = class _AttributeLease {
69
102
  }
70
103
  return(element) {
71
104
  const held = this.#held.delete(element);
72
- const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
73
- const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
105
+ const known = registry.stacks.get(element)?.has(this.#attribute);
106
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
74
107
  const hold = this.#take(stack);
75
108
  if (!hold) return false;
76
109
  const carries = element.getAttribute(this.#attribute) === hold.written;
@@ -88,31 +121,8 @@ var AttributeLease = class _AttributeLease {
88
121
  returnAll() {
89
122
  for (const element of Array.from(this.#held)) this.return(element);
90
123
  }
91
- #stack(element) {
92
- const stacks = _AttributeLease.#registry.stacks;
93
- const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
94
- stacks.set(element, byAttribute);
95
- const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
96
- byAttribute.set(this.#attribute, stack);
97
- return stack;
98
- }
99
- #seed(element) {
100
- const found = [];
101
- for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
102
- const read = readLeaseRecord(element, record);
103
- if (read) found.push({ ...read, record });
104
- }
105
- const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
106
- return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
107
- lease: null,
108
- record,
109
- written,
110
- original,
111
- linked: linked && index > 0
112
- }));
113
- }
114
124
  #take(stack) {
115
- const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
125
+ const hold = findHold(stack, this, this.#record);
116
126
  if (hold) hold.lease = this;
117
127
  return hold;
118
128
  }
@@ -121,7 +131,7 @@ var AttributeLease = class _AttributeLease {
121
131
  const top = stack.at(-1);
122
132
  const bottom = stack[0];
123
133
  const over = top?.written === current ? top : void 0;
124
- const from = this.#base ? bottom : over;
134
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
125
135
  const hold = {
126
136
  lease: this,
127
137
  record: this.#record,
@@ -151,15 +161,11 @@ var AttributeLease = class _AttributeLease {
151
161
  );
152
162
  });
153
163
  for (const [name, recorded] of records) {
154
- if (element.getAttribute(name) === recorded) continue;
155
- if (recorded === null) element.removeAttribute(name);
156
- else element.setAttribute(name, recorded);
164
+ setAttributeIfChanged(element, name, recorded);
157
165
  }
158
166
  }
159
167
  #reflect(element, value) {
160
- if (element.getAttribute(this.#attribute) === value) return;
161
- if (value === null) element.removeAttribute(this.#attribute);
162
- else element.setAttribute(this.#attribute, value);
168
+ setAttributeIfChanged(element, this.#attribute, value);
163
169
  }
164
170
  };
165
171
 
@@ -177,25 +183,25 @@ var EscapeLayer = class _EscapeLayer {
177
183
  #active = null;
178
184
  activate(ownerDocument = document, options) {
179
185
  this.deactivate();
180
- let registry = _EscapeLayer.#registries.get(ownerDocument);
181
- if (!registry) {
182
- registry = _EscapeLayer.#createRegistry();
183
- _EscapeLayer.#registries.set(ownerDocument, registry);
184
- ownerDocument.addEventListener("keydown", registry.onKeydown);
186
+ let registry2 = _EscapeLayer.#registries.get(ownerDocument);
187
+ if (!registry2) {
188
+ registry2 = _EscapeLayer.#createRegistry();
189
+ _EscapeLayer.#registries.set(ownerDocument, registry2);
190
+ ownerDocument.addEventListener("keydown", registry2.onKeydown);
185
191
  }
186
192
  const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
187
- registry.stack.push(entry);
193
+ registry2.stack.push(entry);
188
194
  this.#active = { document: ownerDocument, entry };
189
195
  }
190
196
  deactivate() {
191
197
  const active = this.#active;
192
198
  if (!active) return;
193
- const registry = _EscapeLayer.#registries.get(active.document);
194
- if (registry) {
195
- const index = registry.stack.lastIndexOf(active.entry);
196
- if (index >= 0) registry.stack.splice(index, 1);
197
- if (registry.stack.length === 0) {
198
- active.document.removeEventListener("keydown", registry.onKeydown);
199
+ const registry2 = _EscapeLayer.#registries.get(active.document);
200
+ if (registry2) {
201
+ const index = registry2.stack.lastIndexOf(active.entry);
202
+ if (index >= 0) registry2.stack.splice(index, 1);
203
+ if (registry2.stack.length === 0) {
204
+ active.document.removeEventListener("keydown", registry2.onKeydown);
199
205
  _EscapeLayer.#registries.delete(active.document);
200
206
  }
201
207
  }
@@ -204,22 +210,22 @@ var EscapeLayer = class _EscapeLayer {
204
210
  get ownsEscape() {
205
211
  const active = this.#active;
206
212
  if (!active) return false;
207
- const registry = _EscapeLayer.#registries.get(active.document);
208
- if (!registry) return false;
209
- return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
213
+ const registry2 = _EscapeLayer.#registries.get(active.document);
214
+ if (!registry2) return false;
215
+ return _EscapeLayer.#resolveOwner(registry2.stack) === active.entry;
210
216
  }
211
217
  static #createRegistry() {
212
- const registry = {
218
+ const registry2 = {
213
219
  stack: [],
214
220
  onKeydown: (event) => {
215
221
  if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
216
- const owner = _EscapeLayer.#resolveOwner(registry.stack);
222
+ const owner = _EscapeLayer.#resolveOwner(registry2.stack);
217
223
  if (!owner) return;
218
224
  event.preventDefault();
219
225
  owner.onDismiss();
220
226
  }
221
227
  };
222
- return registry;
228
+ return registry2;
223
229
  }
224
230
  static #resolveOwner(stack) {
225
231
  for (let index = stack.length - 1; index >= 0; index--) {
@@ -259,6 +265,9 @@ var MicrotaskCoalescer = class {
259
265
  this.#queued = false;
260
266
  this.#generation += 1;
261
267
  }
268
+ get pending() {
269
+ return this.#queued;
270
+ }
262
271
  schedule() {
263
272
  if (!this.#active || this.#queued) return;
264
273
  this.#queued = true;
@@ -289,6 +298,9 @@ var MorphRenderWatcher = class {
289
298
  schedule() {
290
299
  this.#pass.schedule();
291
300
  }
301
+ get pending() {
302
+ return this.#pass.pending;
303
+ }
292
304
  disconnect() {
293
305
  this.#listeners.dispose();
294
306
  this.#pass.cancel();
@@ -323,10 +335,11 @@ var DropdownController = class extends Controller {
323
335
  this.#close("outside");
324
336
  }
325
337
  };
338
+ #listeners = new ListenerSet();
326
339
  connect() {
327
340
  this.#associateTriggerWithMenu();
328
341
  this.#close("api");
329
- document.addEventListener("click", this.#onOutsideClick, true);
342
+ this.#listeners.add(document, "click", this.#onOutsideClick, { capture: true });
330
343
  this.#morphRender.observe(this.element);
331
344
  this.#connected = true;
332
345
  this.#reporting = true;
@@ -335,7 +348,7 @@ var DropdownController = class extends Controller {
335
348
  this.#connected = false;
336
349
  this.#reporting = false;
337
350
  this.#escapeLayer.deactivate();
338
- document.removeEventListener("click", this.#onOutsideClick, true);
351
+ this.#listeners.dispose();
339
352
  this.#morphRender.disconnect();
340
353
  }
341
354
  triggerTargetConnected() {
@@ -1,15 +1,26 @@
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
+ if (value === null) element.removeAttribute(name);
6
+ else element.setAttribute(name, value);
7
+ return true;
8
+ }
9
+
3
10
  function sharedRegistry(key, create) {
4
11
  const symbol = Symbol.for(key);
5
12
  const scope = globalThis;
6
13
  const existing = scope[symbol];
7
14
  if (existing !== void 0) return existing;
8
- const registry = create();
9
- Object.defineProperty(globalThis, symbol, { value: registry });
10
- return registry;
15
+ const registry2 = create();
16
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
17
+ return registry2;
11
18
  }
12
19
 
20
+ var registry = sharedRegistry(
21
+ "stimeo-ui.attribute-lease.registry.v1",
22
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
23
+ );
13
24
  function isAttributeValue(value) {
14
25
  return value === null || typeof value === "string";
15
26
  }
@@ -26,24 +37,46 @@ function readLeaseRecord(element, record) {
26
37
  return void 0;
27
38
  }
28
39
  }
29
- var AttributeLease = class _AttributeLease {
30
- static #registry = sharedRegistry(
31
- "stimeo-ui.attribute-lease.registry.v1",
32
- () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
33
- );
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 {
34
65
  #attribute;
35
66
  #record;
36
67
  #base;
68
+ #preserveAuthor;
37
69
  #held = /* @__PURE__ */ new Set();
38
70
  constructor(attribute, owner, options = {}) {
39
71
  this.#attribute = attribute;
40
72
  this.#record = `data-${owner}-${attribute}-lease`;
41
73
  this.#base = options.base === true;
42
- const names = _AttributeLease.#registry.names;
74
+ this.#preserveAuthor = options.preserveAuthor === true;
75
+ const names = registry.names;
43
76
  names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
44
77
  }
45
78
  write(element, value) {
46
- const stack = this.#stack(element);
79
+ const stack = attributeStack(element, this.#attribute);
47
80
  const hold = this.#take(stack) ?? this.#acquire(element, stack);
48
81
  this.#held.add(element);
49
82
  const shows = !this.#covered(element, stack, hold);
@@ -53,8 +86,8 @@ var AttributeLease = class _AttributeLease {
53
86
  }
54
87
  return(element) {
55
88
  const held = this.#held.delete(element);
56
- const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
57
- const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
89
+ const known = registry.stacks.get(element)?.has(this.#attribute);
90
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
58
91
  const hold = this.#take(stack);
59
92
  if (!hold) return false;
60
93
  const carries = element.getAttribute(this.#attribute) === hold.written;
@@ -72,31 +105,8 @@ var AttributeLease = class _AttributeLease {
72
105
  returnAll() {
73
106
  for (const element of Array.from(this.#held)) this.return(element);
74
107
  }
75
- #stack(element) {
76
- const stacks = _AttributeLease.#registry.stacks;
77
- const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
78
- stacks.set(element, byAttribute);
79
- const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
80
- byAttribute.set(this.#attribute, stack);
81
- return stack;
82
- }
83
- #seed(element) {
84
- const found = [];
85
- for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
86
- const read = readLeaseRecord(element, record);
87
- if (read) found.push({ ...read, record });
88
- }
89
- const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
90
- return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
91
- lease: null,
92
- record,
93
- written,
94
- original,
95
- linked: linked && index > 0
96
- }));
97
- }
98
108
  #take(stack) {
99
- const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
109
+ const hold = findHold(stack, this, this.#record);
100
110
  if (hold) hold.lease = this;
101
111
  return hold;
102
112
  }
@@ -105,7 +115,7 @@ var AttributeLease = class _AttributeLease {
105
115
  const top = stack.at(-1);
106
116
  const bottom = stack[0];
107
117
  const over = top?.written === current ? top : void 0;
108
- const from = this.#base ? bottom : over;
118
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
109
119
  const hold = {
110
120
  lease: this,
111
121
  record: this.#record,
@@ -135,15 +145,11 @@ var AttributeLease = class _AttributeLease {
135
145
  );
136
146
  });
137
147
  for (const [name, recorded] of records) {
138
- if (element.getAttribute(name) === recorded) continue;
139
- if (recorded === null) element.removeAttribute(name);
140
- else element.setAttribute(name, recorded);
148
+ setAttributeIfChanged(element, name, recorded);
141
149
  }
142
150
  }
143
151
  #reflect(element, value) {
144
- if (element.getAttribute(this.#attribute) === value) return;
145
- if (value === null) element.removeAttribute(this.#attribute);
146
- else element.setAttribute(this.#attribute, value);
152
+ setAttributeIfChanged(element, this.#attribute, value);
147
153
  }
148
154
  };
149
155
 
@@ -153,66 +159,196 @@ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
153
159
  "deleteCompositionText",
154
160
  "deleteByComposition"
155
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;
156
168
  var CompositionTracker = class {
157
169
  #observedTargets = /* @__PURE__ */ new Set();
158
- #activeTargets = /* @__PURE__ */ new Set();
170
+ #composition = null;
159
171
  #onStart;
160
172
  #onEnd;
173
+ #onAbandon;
161
174
  #confirmedTarget = null;
175
+ #keyComposing = false;
176
+ #connected = false;
177
+ #abandoned = null;
162
178
  constructor(options = {}) {
163
179
  this.#onStart = options.onStart;
164
180
  this.#onEnd = options.onEnd;
181
+ this.#onAbandon = options.onAbandon;
165
182
  }
166
183
  observe(target) {
184
+ this.#connected = true;
167
185
  if (this.#observedTargets.has(target)) return;
168
186
  target.addEventListener("compositionstart", this.#handleStart);
169
187
  target.addEventListener("compositionend", this.#handleEnd);
170
- 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);
171
193
  this.#observedTargets.add(target);
172
194
  }
173
195
  unobserve(target) {
174
196
  if (!this.#observedTargets.delete(target)) return;
175
- target.removeEventListener("compositionstart", this.#handleStart);
176
- target.removeEventListener("compositionend", this.#handleEnd);
177
- target.removeEventListener("keydown", this.#handleKeydown);
178
- this.#activeTargets.delete(target);
197
+ this.#release(target);
198
+ if (this.#composition?.target === target) this.#letGo();
179
199
  if (this.#confirmedTarget === target) this.#confirmedTarget = null;
180
200
  }
181
201
  disconnect() {
182
- for (const target of this.#observedTargets) {
183
- target.removeEventListener("compositionstart", this.#handleStart);
184
- target.removeEventListener("compositionend", this.#handleEnd);
185
- target.removeEventListener("keydown", this.#handleKeydown);
186
- }
202
+ this.#connected = false;
203
+ for (const target of this.#observedTargets) this.#release(target);
187
204
  this.#observedTargets.clear();
188
- this.#activeTargets.clear();
205
+ this.#letGo();
189
206
  this.#confirmedTarget = null;
207
+ this.#keyComposing = false;
208
+ this.#abandoned = null;
190
209
  }
191
210
  isComposing(event) {
192
- return this.#activeTargets.size > 0 || event?.isComposing === true;
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;
193
221
  }
194
222
  consumesConfirmedInput(event) {
195
223
  const confirmed = this.#confirmedTarget;
196
224
  this.#confirmedTarget = null;
197
225
  if (confirmed === null || confirmed !== event.target) return false;
198
226
  const inputType = event.inputType;
199
- return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
227
+ return !inputType || isCompositionInputType(inputType);
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
+ }
200
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
+ };
201
280
  #handleStart = (event) => {
202
281
  this.#confirmedTarget = null;
203
- 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();
204
288
  this.#onStart?.(event);
205
289
  };
206
290
  #handleEnd = (event) => {
207
- if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
291
+ this.#runAbandon();
292
+ if (this.#composition?.target === event.currentTarget) this.#composition = null;
208
293
  this.#confirmedTarget = event.target;
209
294
  this.#onEnd?.(event);
210
295
  };
211
- #handleKeydown = () => {
296
+ #handleKeydown = (event) => {
297
+ this.#settle();
298
+ this.#runAbandon();
212
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();
213
338
  };
214
339
  };
215
340
 
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
+
216
352
  var EditableController = class extends Controller {
217
353
  static targets = ["display", "input"];
218
354
  static values = {
@@ -232,17 +368,18 @@ var EditableController = class extends Controller {
232
368
  if (next instanceof Node && this.element.contains(next)) return;
233
369
  if (this.submitOnBlurValue) this.#commit(false);
234
370
  };
371
+ #listeners = new ListenerSet();
235
372
  connect() {
236
373
  this.#connected = true;
237
374
  if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
238
- this.element.addEventListener("focusout", this.#onFocusOut);
375
+ this.#listeners.add(this.element, "focusout", this.#onFocusOut);
239
376
  this.#revertValue = null;
240
377
  this.#setMode("display");
241
378
  }
242
379
  disconnect() {
243
380
  this.#connected = false;
244
381
  this.#composition.disconnect();
245
- this.element.removeEventListener("focusout", this.#onFocusOut);
382
+ this.#listeners.dispose();
246
383
  }
247
384
  inputTargetConnected(input) {
248
385
  this.#composition.observe(input);