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
@@ -97,6 +97,17 @@ var KeyedTimers = class {
97
97
  }
98
98
  };
99
99
 
100
+ var ListenerSet = class {
101
+ #abort = new AbortController();
102
+ add(target, type, handler, options) {
103
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
104
+ }
105
+ dispose() {
106
+ this.#abort.abort();
107
+ this.#abort = new AbortController();
108
+ }
109
+ };
110
+
100
111
  var MicrotaskCoalescer = class {
101
112
  #run;
102
113
  #queued = false;
@@ -113,6 +124,9 @@ var MicrotaskCoalescer = class {
113
124
  this.#queued = false;
114
125
  this.#generation += 1;
115
126
  }
127
+ get pending() {
128
+ return this.#queued;
129
+ }
116
130
  schedule() {
117
131
  if (!this.#active || this.#queued) return;
118
132
  this.#queued = true;
@@ -169,6 +183,94 @@ var NumberValueReader = class {
169
183
  }
170
184
  };
171
185
 
186
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
187
+ if (element.getAttribute(name) === value) return false;
188
+ if (value === null) element.removeAttribute(name);
189
+ else element.setAttribute(name, value);
190
+ return true;
191
+ }
192
+
193
+ var OwnedText = class {
194
+ #attribute;
195
+ #channel;
196
+ #guard;
197
+ #writes = /* @__PURE__ */ new WeakMap();
198
+ constructor(attribute, channel = "text", guard = () => true) {
199
+ this.#attribute = attribute;
200
+ this.#channel = channel;
201
+ this.#guard = guard;
202
+ }
203
+ hasRecord(element) {
204
+ return element.hasAttribute(this.#attribute);
205
+ }
206
+ read(element, evidence = {}) {
207
+ if (evidence.source === "live") return this.#writes.get(element) ?? null;
208
+ const raw = element.getAttribute(this.#attribute);
209
+ if (raw === null) return null;
210
+ try {
211
+ const record = JSON.parse(raw);
212
+ if (!Array.isArray(record) || record.length !== 3 || record[0] !== this.#channel || typeof record[1] !== "string" || typeof record[2] !== "string")
213
+ return null;
214
+ return { original: record[1], written: record[2] };
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+ owns(element, evidence = {}) {
220
+ return this.#ownedRecord(element, evidence) !== null;
221
+ }
222
+ #ownedRecord(element, evidence) {
223
+ const record = this.read(element, evidence);
224
+ if (record === null || !this.#guard(element)) return null;
225
+ if (this.#channel === "text" && element.firstElementChild !== null) return null;
226
+ const current = this.#current(element);
227
+ return current === record.written || evidence.source === "live" && evidence.allowOriginal === true && current === record.original ? record : null;
228
+ }
229
+ write(element, written, original, force = false) {
230
+ const current = this.#current(element);
231
+ if (current === null) return;
232
+ const before = original ?? this.#writes.get(element)?.original ?? this.read(element)?.original ?? current;
233
+ if (force || current !== written) this.#put(element, written);
234
+ this.remember(element, written, before);
235
+ }
236
+ remember(element, written, original) {
237
+ const current = this.#current(element);
238
+ if (current === null) return;
239
+ const before = original ?? this.#writes.get(element)?.original ?? this.read(element)?.original ?? current;
240
+ this.#writes.set(element, { original: before, written });
241
+ setAttributeIfChanged(
242
+ element,
243
+ this.#attribute,
244
+ JSON.stringify([this.#channel, before, written])
245
+ );
246
+ }
247
+ clear(element, evidence = {}) {
248
+ if (!this.owns(element, evidence)) return false;
249
+ if (this.#current(element) !== "") this.#put(element, "");
250
+ this.forget(element);
251
+ return true;
252
+ }
253
+ restore(element, evidence = {}) {
254
+ const record = this.#ownedRecord(element, evidence);
255
+ if (record === null) return false;
256
+ if (this.#current(element) !== record.original) this.#put(element, record.original);
257
+ this.forget(element);
258
+ return true;
259
+ }
260
+ forget(element) {
261
+ setAttributeIfChanged(element, this.#attribute, null);
262
+ this.#writes.delete(element);
263
+ }
264
+ #current(element) {
265
+ if (this.#channel === "text") return element.textContent ?? "";
266
+ return element instanceof HTMLInputElement ? element.value : null;
267
+ }
268
+ #put(element, text) {
269
+ if (this.#channel === "text") element.textContent = text;
270
+ else if (element instanceof HTMLInputElement) element.value = text;
271
+ }
272
+ };
273
+
172
274
  var LEVELS = ["polite", "assertive"];
173
275
  var STAND_IN = "stand-in";
174
276
  var ANNOUNCED = "announced";
@@ -185,14 +287,13 @@ var AnnouncerController = class _AnnouncerController extends Controller {
185
287
  static actions = ["announce"];
186
288
  #timers = new SafeTimeout();
187
289
  #gate = new DetachGate();
290
+ #listeners = new ListenerSet();
188
291
  #regionTimers = new KeyedTimers();
189
292
  #generated = /* @__PURE__ */ new Map();
190
293
  get #standIn() {
191
294
  return `data-${this.identifier}-${STAND_IN}`;
192
295
  }
193
- get #announced() {
194
- return `data-${this.identifier}-${ANNOUNCED}`;
195
- }
296
+ #messageText = new OwnedText(`data-${this.identifier}-${ANNOUNCED}`);
196
297
  #queues = /* @__PURE__ */ new Map();
197
298
  #draining = /* @__PURE__ */ new Set();
198
299
  #reconcile = new MicrotaskCoalescer(() => this.#reconcileRegions());
@@ -217,8 +318,8 @@ var AnnouncerController = class _AnnouncerController extends Controller {
217
318
  this.#emptyInheritedMessages();
218
319
  this.#reconcileRegions();
219
320
  this.#hostWatch.observe(this.element, { childList: true });
220
- this.element.addEventListener("stimeo--announcer:announce", this.#onAnnounceEvent);
221
- window.addEventListener("stimeo--announcer:announce", this.#onAnnounceEvent);
321
+ this.#listeners.add(this.element, "stimeo--announcer:announce", this.#onAnnounceEvent);
322
+ this.#listeners.add(window, "stimeo--announcer:announce", this.#onAnnounceEvent);
222
323
  }
223
324
  disconnect() {
224
325
  this.#gate.disconnected(this, () => this.#teardown());
@@ -226,8 +327,7 @@ var AnnouncerController = class _AnnouncerController extends Controller {
226
327
  #teardown() {
227
328
  this.#reconcile.cancel();
228
329
  this.#hostWatch.disconnect();
229
- this.element.removeEventListener("stimeo--announcer:announce", this.#onAnnounceEvent);
230
- window.removeEventListener("stimeo--announcer:announce", this.#onAnnounceEvent);
330
+ this.#listeners.dispose();
231
331
  this.#timers.clearAll();
232
332
  this.#regionTimers.clearAll();
233
333
  this.#queues.clear();
@@ -279,9 +379,7 @@ var AnnouncerController = class _AnnouncerController extends Controller {
279
379
  }
280
380
  #emptyInheritedMessages() {
281
381
  for (const region of [...this.politeTargets, ...this.assertiveTargets]) {
282
- if (!region.hasAttribute(this.#announced)) continue;
283
- region.replaceChildren();
284
- region.removeAttribute(this.#announced);
382
+ this.#messageText.clear(region);
285
383
  }
286
384
  }
287
385
  #removeGenerated() {
@@ -325,24 +423,22 @@ var AnnouncerController = class _AnnouncerController extends Controller {
325
423
  }
326
424
  if (this.dedupeReannounceValue && region.textContent === message) {
327
425
  region.textContent = "";
426
+ this.#messageText.forget(region);
328
427
  this.#scheduleDrain(level);
329
428
  return;
330
429
  }
331
430
  queue.shift();
332
431
  this.#regionTimers.clear(region);
333
- region.textContent = message;
334
- region.setAttribute(this.#announced, "");
335
- this.#scheduleClear(region, message);
432
+ this.#messageText.write(region, message, void 0, true);
433
+ this.#scheduleClear(region);
336
434
  if (queue.length > 0) this.#scheduleDrain(level);
337
435
  }
338
- #scheduleClear(region, message) {
436
+ #scheduleClear(region) {
339
437
  if (this.#safeClearAfter <= 0) return;
340
438
  this.#regionTimers.set(
341
439
  region,
342
440
  () => {
343
- if (region.textContent !== message) return;
344
- region.textContent = "";
345
- region.removeAttribute(this.#announced);
441
+ this.#messageText.clear(region);
346
442
  },
347
443
  this.#safeClearAfter
348
444
  );
@@ -10,16 +10,27 @@ function announce(message, options = {}) {
10
10
  );
11
11
  }
12
12
 
13
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
14
+ if (element.getAttribute(name) === value) return false;
15
+ if (value === null) element.removeAttribute(name);
16
+ else element.setAttribute(name, value);
17
+ return true;
18
+ }
19
+
13
20
  function sharedRegistry(key, create) {
14
21
  const symbol = Symbol.for(key);
15
22
  const scope = globalThis;
16
23
  const existing = scope[symbol];
17
24
  if (existing !== void 0) return existing;
18
- const registry = create();
19
- Object.defineProperty(globalThis, symbol, { value: registry });
20
- return registry;
25
+ const registry2 = create();
26
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
27
+ return registry2;
21
28
  }
22
29
 
30
+ var registry = sharedRegistry(
31
+ "stimeo-ui.attribute-lease.registry.v1",
32
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
33
+ );
23
34
  function isAttributeValue(value) {
24
35
  return value === null || typeof value === "string";
25
36
  }
@@ -36,24 +47,46 @@ function readLeaseRecord(element, record) {
36
47
  return void 0;
37
48
  }
38
49
  }
39
- var AttributeLease = class _AttributeLease {
40
- static #registry = sharedRegistry(
41
- "stimeo-ui.attribute-lease.registry.v1",
42
- () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
43
- );
50
+ function attributeStack(element, attribute) {
51
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
52
+ registry.stacks.set(element, byAttribute);
53
+ const known = byAttribute.get(attribute);
54
+ if (known) return known;
55
+ const found = [];
56
+ for (const record of registry.names.get(attribute) ?? []) {
57
+ const read = readLeaseRecord(element, record);
58
+ if (read) found.push({ ...read, record });
59
+ }
60
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
61
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
62
+ lease: null,
63
+ record,
64
+ written,
65
+ original,
66
+ linked: linked && index > 0
67
+ }));
68
+ byAttribute.set(attribute, stack);
69
+ return stack;
70
+ }
71
+ function findHold(stack, lease, record) {
72
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
73
+ }
74
+ var AttributeLease = class {
44
75
  #attribute;
45
76
  #record;
46
77
  #base;
78
+ #preserveAuthor;
47
79
  #held = /* @__PURE__ */ new Set();
48
80
  constructor(attribute, owner, options = {}) {
49
81
  this.#attribute = attribute;
50
82
  this.#record = `data-${owner}-${attribute}-lease`;
51
83
  this.#base = options.base === true;
52
- const names = _AttributeLease.#registry.names;
84
+ this.#preserveAuthor = options.preserveAuthor === true;
85
+ const names = registry.names;
53
86
  names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
54
87
  }
55
88
  write(element, value) {
56
- const stack = this.#stack(element);
89
+ const stack = attributeStack(element, this.#attribute);
57
90
  const hold = this.#take(stack) ?? this.#acquire(element, stack);
58
91
  this.#held.add(element);
59
92
  const shows = !this.#covered(element, stack, hold);
@@ -63,8 +96,8 @@ var AttributeLease = class _AttributeLease {
63
96
  }
64
97
  return(element) {
65
98
  const held = this.#held.delete(element);
66
- const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
67
- const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
99
+ const known = registry.stacks.get(element)?.has(this.#attribute);
100
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
68
101
  const hold = this.#take(stack);
69
102
  if (!hold) return false;
70
103
  const carries = element.getAttribute(this.#attribute) === hold.written;
@@ -82,31 +115,8 @@ var AttributeLease = class _AttributeLease {
82
115
  returnAll() {
83
116
  for (const element of Array.from(this.#held)) this.return(element);
84
117
  }
85
- #stack(element) {
86
- const stacks = _AttributeLease.#registry.stacks;
87
- const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
88
- stacks.set(element, byAttribute);
89
- const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
90
- byAttribute.set(this.#attribute, stack);
91
- return stack;
92
- }
93
- #seed(element) {
94
- const found = [];
95
- for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
96
- const read = readLeaseRecord(element, record);
97
- if (read) found.push({ ...read, record });
98
- }
99
- const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
100
- return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
101
- lease: null,
102
- record,
103
- written,
104
- original,
105
- linked: linked && index > 0
106
- }));
107
- }
108
118
  #take(stack) {
109
- const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
119
+ const hold = findHold(stack, this, this.#record);
110
120
  if (hold) hold.lease = this;
111
121
  return hold;
112
122
  }
@@ -115,7 +125,7 @@ var AttributeLease = class _AttributeLease {
115
125
  const top = stack.at(-1);
116
126
  const bottom = stack[0];
117
127
  const over = top?.written === current ? top : void 0;
118
- const from = this.#base ? bottom : over;
128
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
119
129
  const hold = {
120
130
  lease: this,
121
131
  record: this.#record,
@@ -145,15 +155,11 @@ var AttributeLease = class _AttributeLease {
145
155
  );
146
156
  });
147
157
  for (const [name, recorded] of records) {
148
- if (element.getAttribute(name) === recorded) continue;
149
- if (recorded === null) element.removeAttribute(name);
150
- else element.setAttribute(name, recorded);
158
+ setAttributeIfChanged(element, name, recorded);
151
159
  }
152
160
  }
153
161
  #reflect(element, value) {
154
- if (element.getAttribute(this.#attribute) === value) return;
155
- if (value === null) element.removeAttribute(this.#attribute);
156
- else element.setAttribute(this.#attribute, value);
162
+ setAttributeIfChanged(element, this.#attribute, value);
157
163
  }
158
164
  };
159
165
 
@@ -163,63 +169,182 @@ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
163
169
  "deleteCompositionText",
164
170
  "deleteByComposition"
165
171
  ]);
172
+ function isCompositionInputType(inputType) {
173
+ return inputType !== void 0 && COMPOSITION_INPUT_TYPES.has(inputType);
174
+ }
175
+ var BEFORE_MORPH_ATTRIBUTE = "turbo:before-morph-attribute";
176
+ var textOf = (element) => element.value;
177
+ var holdsFocus = (element) => element instanceof Node && element.getRootNode().activeElement === element;
166
178
  var CompositionTracker = class {
167
179
  #observedTargets = /* @__PURE__ */ new Set();
168
- #activeTargets = /* @__PURE__ */ new Set();
180
+ #composition = null;
169
181
  #onStart;
170
182
  #onEnd;
183
+ #onAbandon;
171
184
  #confirmedTarget = null;
185
+ #keyComposing = false;
186
+ #connected = false;
187
+ #abandoned = null;
172
188
  constructor(options = {}) {
173
189
  this.#onStart = options.onStart;
174
190
  this.#onEnd = options.onEnd;
191
+ this.#onAbandon = options.onAbandon;
175
192
  }
176
193
  observe(target) {
194
+ this.#connected = true;
177
195
  if (this.#observedTargets.has(target)) return;
178
196
  target.addEventListener("compositionstart", this.#handleStart);
179
197
  target.addEventListener("compositionend", this.#handleEnd);
180
- target.addEventListener("keydown", this.#handleKeydown);
198
+ target.addEventListener("keydown", this.#handleKeydown, true);
199
+ target.addEventListener("beforeinput", this.#handleBeforeInput, true);
200
+ target.addEventListener("input", this.#handleInput, true);
201
+ target.addEventListener("blur", this.#handleBlur, true);
202
+ target.addEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
181
203
  this.#observedTargets.add(target);
182
204
  }
183
205
  unobserve(target) {
184
206
  if (!this.#observedTargets.delete(target)) return;
185
- target.removeEventListener("compositionstart", this.#handleStart);
186
- target.removeEventListener("compositionend", this.#handleEnd);
187
- target.removeEventListener("keydown", this.#handleKeydown);
188
- this.#activeTargets.delete(target);
207
+ this.#release(target);
208
+ if (this.#composition?.target === target) this.#letGo();
189
209
  if (this.#confirmedTarget === target) this.#confirmedTarget = null;
190
210
  }
191
211
  disconnect() {
192
- for (const target of this.#observedTargets) {
193
- target.removeEventListener("compositionstart", this.#handleStart);
194
- target.removeEventListener("compositionend", this.#handleEnd);
195
- target.removeEventListener("keydown", this.#handleKeydown);
196
- }
212
+ this.#connected = false;
213
+ for (const target of this.#observedTargets) this.#release(target);
197
214
  this.#observedTargets.clear();
198
- this.#activeTargets.clear();
215
+ this.#letGo();
199
216
  this.#confirmedTarget = null;
217
+ this.#keyComposing = false;
218
+ this.#abandoned = null;
200
219
  }
201
220
  isComposing(event) {
202
- return this.#activeTargets.size > 0 || event?.isComposing === true;
221
+ this.#settle();
222
+ return event?.isComposing === true || this.#composition !== null;
223
+ }
224
+ composingElement() {
225
+ this.#settle();
226
+ return this.#composition?.element ?? null;
227
+ }
228
+ owns(event) {
229
+ const element = this.composingElement();
230
+ return event.isComposing === true || element !== null && element === event.target;
203
231
  }
204
232
  consumesConfirmedInput(event) {
205
233
  const confirmed = this.#confirmedTarget;
206
234
  this.#confirmedTarget = null;
207
235
  if (confirmed === null || confirmed !== event.target) return false;
208
236
  const inputType = event.inputType;
209
- return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
237
+ return !inputType || isCompositionInputType(inputType);
238
+ }
239
+ #release(target) {
240
+ target.removeEventListener("compositionstart", this.#handleStart);
241
+ target.removeEventListener("compositionend", this.#handleEnd);
242
+ target.removeEventListener("keydown", this.#handleKeydown, true);
243
+ target.removeEventListener("beforeinput", this.#handleBeforeInput, true);
244
+ target.removeEventListener("input", this.#handleInput, true);
245
+ target.removeEventListener("blur", this.#handleBlur, true);
246
+ target.removeEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
247
+ }
248
+ #letGo() {
249
+ const composition = this.#composition;
250
+ if (composition === null) return;
251
+ if (!holdsFocus(composition.element)) {
252
+ this.#composition = null;
253
+ return;
254
+ }
255
+ queueMicrotask(() => {
256
+ if (this.#composition !== composition || this.#observedTargets.has(composition.target)) {
257
+ return;
258
+ }
259
+ this.#composition = null;
260
+ if (!this.#connected) return;
261
+ this.#abandoned = { element: composition.element, inPlace: true };
262
+ this.#runAbandon();
263
+ });
264
+ }
265
+ #settle() {
266
+ const composition = this.#composition;
267
+ if (composition === null) return;
268
+ const { target, element, text } = composition;
269
+ if (target instanceof Node && element instanceof Node && !target.contains(element)) {
270
+ this.#composition = null;
271
+ } else if (textOf(element) !== text) {
272
+ this.#abandon(false);
273
+ }
274
+ }
275
+ #abandon(inPlace) {
276
+ this.#owe(inPlace);
277
+ this.#composition = null;
278
+ queueMicrotask(this.#runAbandon);
210
279
  }
280
+ #owe(inPlace) {
281
+ const composition = this.#composition;
282
+ if (composition !== null) this.#abandoned = { element: composition.element, inPlace };
283
+ }
284
+ #runAbandon = () => {
285
+ const abandoned = this.#abandoned;
286
+ if (abandoned === null) return;
287
+ this.#abandoned = null;
288
+ this.#onAbandon?.(abandoned.element, abandoned.inPlace);
289
+ };
211
290
  #handleStart = (event) => {
212
291
  this.#confirmedTarget = null;
213
- if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
292
+ this.#settle();
293
+ this.#owe(true);
294
+ const target = event.currentTarget;
295
+ const element = event.target;
296
+ this.#composition = { target, element, text: textOf(element) };
297
+ this.#runAbandon();
214
298
  this.#onStart?.(event);
215
299
  };
216
300
  #handleEnd = (event) => {
217
- if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
301
+ this.#runAbandon();
302
+ if (this.#composition?.target === event.currentTarget) this.#composition = null;
218
303
  this.#confirmedTarget = event.target;
219
304
  this.#onEnd?.(event);
220
305
  };
221
- #handleKeydown = () => {
306
+ #handleKeydown = (event) => {
307
+ this.#settle();
308
+ this.#runAbandon();
222
309
  this.#confirmedTarget = null;
310
+ this.#keyComposing = event.isComposing === true;
311
+ };
312
+ #handleBeforeInput = (event) => {
313
+ const keyComposing = this.#keyComposing;
314
+ this.#keyComposing = false;
315
+ this.#settle();
316
+ const { isComposing, inputType } = event;
317
+ if (this.#composition !== null && !keyComposing && !isComposing && inputType && !isCompositionInputType(inputType)) {
318
+ this.#abandon(true);
319
+ }
320
+ this.#runAbandon();
321
+ };
322
+ #handleInput = (event) => {
323
+ const composition = this.#composition;
324
+ const { isTrusted, isComposing, inputType } = event;
325
+ if (composition !== null && event.target === composition.element && (isTrusted || isComposing || isCompositionInputType(inputType))) {
326
+ composition.text = textOf(composition.element);
327
+ } else {
328
+ this.#settle();
329
+ }
330
+ this.#runAbandon();
331
+ };
332
+ #handleBlur = (event) => {
333
+ this.#settle();
334
+ this.#runAbandon();
335
+ const composition = this.#composition;
336
+ if (composition === null || composition.target === composition.element) return;
337
+ const { element } = composition;
338
+ if (!(element instanceof Node) || event.target !== element) return;
339
+ const root = element.getRootNode();
340
+ if (root.activeElement !== element) this.#composition = null;
341
+ };
342
+ #handleBeforeMorphAttribute = (event) => {
343
+ this.#settle();
344
+ const composition = this.#composition;
345
+ if (composition?.target !== event.currentTarget || event.target !== composition.element) return;
346
+ const detail = event.detail;
347
+ if (detail?.attributeName === "value") event.preventDefault();
223
348
  };
224
349
  };
225
350