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
@@ -82,6 +82,9 @@ var MicrotaskCoalescer = class {
82
82
  this.#queued = false;
83
83
  this.#generation += 1;
84
84
  }
85
+ get pending() {
86
+ return this.#queued;
87
+ }
85
88
  schedule() {
86
89
  if (!this.#active || this.#queued) return;
87
90
  this.#queued = true;
@@ -112,6 +115,9 @@ var MorphRenderWatcher = class {
112
115
  schedule() {
113
116
  this.#pass.schedule();
114
117
  }
118
+ get pending() {
119
+ return this.#pass.pending;
120
+ }
115
121
  disconnect() {
116
122
  this.#listeners.dispose();
117
123
  this.#pass.cancel();
@@ -167,6 +173,13 @@ function prefersReducedMotion() {
167
173
  return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
168
174
  }
169
175
 
176
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
177
+ if (element.getAttribute(name) === value) return false;
178
+ if (value === null) element.removeAttribute(name);
179
+ else element.setAttribute(name, value);
180
+ return true;
181
+ }
182
+
170
183
  function sharedRegistry(key, create) {
171
184
  const symbol = Symbol.for(key);
172
185
  const scope = globalThis;
@@ -221,9 +234,8 @@ var StateRegions = class _StateRegions {
221
234
  const below = hold.linked ? stack[index - 1] : void 0;
222
235
  if (below) {
223
236
  if (region.hidden !== below.written) region.hidden = below.written;
224
- } else if (region.getAttribute("hidden") !== hold.authored) {
225
- if (hold.authored === null) region.removeAttribute("hidden");
226
- else region.setAttribute("hidden", hold.authored);
237
+ } else {
238
+ setAttributeIfChanged(region, "hidden", hold.authored);
227
239
  }
228
240
  }
229
241
  this.#mark(region, stack);
@@ -298,9 +310,7 @@ var StateRegions = class _StateRegions {
298
310
  );
299
311
  });
300
312
  for (const [name, recorded] of records) {
301
- if (region.getAttribute(name) === recorded) continue;
302
- if (recorded === null) region.removeAttribute(name);
303
- else region.setAttribute(name, recorded);
313
+ setAttributeIfChanged(region, name, recorded);
304
314
  }
305
315
  }
306
316
  #recorded(region, name) {
@@ -357,6 +367,7 @@ var StickToBottomController = class _StickToBottomController extends Controller
357
367
  this.#updatePinned();
358
368
  this.#setHasNew(this.#hasNew);
359
369
  });
370
+ #listeners = new ListenerSet();
360
371
  connect() {
361
372
  this.#morphRender.observe(this.element);
362
373
  this.#connected = true;
@@ -364,14 +375,14 @@ var StickToBottomController = class _StickToBottomController extends Controller
364
375
  this.#setHasNew(false);
365
376
  this.#pinned = this.#isPinned();
366
377
  this.#reflectPinned();
367
- this.element.addEventListener("scroll", this.#onScroll, { passive: true });
378
+ this.#listeners.add(this.element, "scroll", this.#onScroll, { passive: true });
368
379
  this.#syncWatched();
369
380
  if (!this.#measurable()) this.#waitForLayout();
370
381
  }
371
382
  disconnect() {
372
383
  this.#morphRender.disconnect();
373
384
  this.#connected = false;
374
- this.element.removeEventListener("scroll", this.#onScroll);
385
+ this.#listeners.dispose();
375
386
  this.#stopWatching();
376
387
  this.#stopWaitingForLayout();
377
388
  }
@@ -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
 
@@ -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
 
@@ -251,6 +257,87 @@ var NumberValueReader = class {
251
257
  }
252
258
  };
253
259
 
260
+ var OwnedText = class {
261
+ #attribute;
262
+ #channel;
263
+ #guard;
264
+ #writes = /* @__PURE__ */ new WeakMap();
265
+ constructor(attribute, channel = "text", guard = () => true) {
266
+ this.#attribute = attribute;
267
+ this.#channel = channel;
268
+ this.#guard = guard;
269
+ }
270
+ hasRecord(element) {
271
+ return element.hasAttribute(this.#attribute);
272
+ }
273
+ read(element, evidence = {}) {
274
+ if (evidence.source === "live") return this.#writes.get(element) ?? null;
275
+ const raw = element.getAttribute(this.#attribute);
276
+ if (raw === null) return null;
277
+ try {
278
+ const record = JSON.parse(raw);
279
+ if (!Array.isArray(record) || record.length !== 3 || record[0] !== this.#channel || typeof record[1] !== "string" || typeof record[2] !== "string")
280
+ return null;
281
+ return { original: record[1], written: record[2] };
282
+ } catch {
283
+ return null;
284
+ }
285
+ }
286
+ owns(element, evidence = {}) {
287
+ return this.#ownedRecord(element, evidence) !== null;
288
+ }
289
+ #ownedRecord(element, evidence) {
290
+ const record = this.read(element, evidence);
291
+ if (record === null || !this.#guard(element)) return null;
292
+ if (this.#channel === "text" && element.firstElementChild !== null) return null;
293
+ const current = this.#current(element);
294
+ return current === record.written || evidence.source === "live" && evidence.allowOriginal === true && current === record.original ? record : null;
295
+ }
296
+ write(element, written, original, force = false) {
297
+ const current = this.#current(element);
298
+ if (current === null) return;
299
+ const before = original ?? this.#writes.get(element)?.original ?? this.read(element)?.original ?? current;
300
+ if (force || current !== written) this.#put(element, written);
301
+ this.remember(element, written, before);
302
+ }
303
+ remember(element, written, original) {
304
+ const current = this.#current(element);
305
+ if (current === null) return;
306
+ const before = original ?? this.#writes.get(element)?.original ?? this.read(element)?.original ?? current;
307
+ this.#writes.set(element, { original: before, written });
308
+ setAttributeIfChanged(
309
+ element,
310
+ this.#attribute,
311
+ JSON.stringify([this.#channel, before, written])
312
+ );
313
+ }
314
+ clear(element, evidence = {}) {
315
+ if (!this.owns(element, evidence)) return false;
316
+ if (this.#current(element) !== "") this.#put(element, "");
317
+ this.forget(element);
318
+ return true;
319
+ }
320
+ restore(element, evidence = {}) {
321
+ const record = this.#ownedRecord(element, evidence);
322
+ if (record === null) return false;
323
+ if (this.#current(element) !== record.original) this.#put(element, record.original);
324
+ this.forget(element);
325
+ return true;
326
+ }
327
+ forget(element) {
328
+ setAttributeIfChanged(element, this.#attribute, null);
329
+ this.#writes.delete(element);
330
+ }
331
+ #current(element) {
332
+ if (this.#channel === "text") return element.textContent ?? "";
333
+ return element instanceof HTMLInputElement ? element.value : null;
334
+ }
335
+ #put(element, text) {
336
+ if (this.#channel === "text") element.textContent = text;
337
+ else if (element instanceof HTMLInputElement) element.value = text;
338
+ }
339
+ };
340
+
254
341
  var TimerRegistry = class {
255
342
  ids = /* @__PURE__ */ new Set();
256
343
  clear(id) {
@@ -289,6 +376,8 @@ var BUTTON_BUSY_LABEL = "data-submit-once-busy-label";
289
376
  var LABEL_RECORD = "label";
290
377
  var SUBMITTER_MARK = "submitter";
291
378
  var SubmitOnceController = class _SubmitOnceController extends Controller {
379
+ #textLabel = new OwnedText(this.#labelRecord);
380
+ #valueLabel = new OwnedText(this.#labelRecord, "value");
292
381
  #numbers = new NumberValueReader();
293
382
  static targets = ["submit", "idle", "busy"];
294
383
  static values = {
@@ -379,22 +468,9 @@ var SubmitOnceController = class _SubmitOnceController extends Controller {
379
468
  return copied;
380
469
  }
381
470
  #restoreLabel(control) {
382
- const raw = control.getAttribute(this.#labelRecord);
383
- control.removeAttribute(this.#labelRecord);
384
- let record;
385
- try {
386
- record = JSON.parse(raw ?? "");
387
- } catch {
388
- return;
389
- }
390
- if (!Array.isArray(record) || record.length !== 2) return;
391
- const [original, written] = record;
392
- if (typeof original !== "string" || typeof written !== "string") return;
393
- if (control instanceof HTMLInputElement) {
394
- if (control.value === written) control.value = original;
395
- } else if (control.childElementCount === 0 && control.textContent === written) {
396
- control.textContent = original;
397
- }
471
+ const owner = control instanceof HTMLInputElement ? this.#valueLabel : this.#textLabel;
472
+ owner.restore(control);
473
+ owner.forget(control);
398
474
  }
399
475
  #ownedForms() {
400
476
  const forms = Array.from(this.element.querySelectorAll("form"));
@@ -479,14 +555,9 @@ var SubmitOnceController = class _SubmitOnceController extends Controller {
479
555
  if (label.length === 0) return;
480
556
  if (control instanceof HTMLInputElement) {
481
557
  if (control.type !== "submit") return;
482
- session.labelWrite = {
483
- channel: "value",
484
- control,
485
- original: control.value,
486
- written: label
487
- };
488
- control.setAttribute(this.#labelRecord, JSON.stringify([control.value, label]));
489
- control.value = label;
558
+ const owner2 = new OwnedText(this.#labelRecord, "value");
559
+ session.labelWrite = { control, owner: owner2 };
560
+ owner2.write(control, label, control.value);
490
561
  return;
491
562
  }
492
563
  if (control.hasAttribute("aria-label")) {
@@ -494,30 +565,19 @@ var SubmitOnceController = class _SubmitOnceController extends Controller {
494
565
  return;
495
566
  }
496
567
  if (control.childElementCount > 0) return;
497
- session.labelWrite = {
498
- channel: "text",
499
- control,
500
- original: control.textContent ?? "",
501
- written: label
502
- };
503
- control.setAttribute(this.#labelRecord, JSON.stringify([control.textContent ?? "", label]));
504
- control.textContent = label;
568
+ const owner = new OwnedText(this.#labelRecord);
569
+ session.labelWrite = { control, owner };
570
+ owner.write(control, label, control.textContent ?? "");
505
571
  }
506
572
  #returnSessionState(session, unfinished = false) {
507
573
  if (session.timeoutId !== null) {
508
574
  this.#timers.clear(session.timeoutId);
509
575
  session.timeoutId = null;
510
576
  }
511
- if (session.labelWrite?.channel === "value") {
512
- const { control, original, written } = session.labelWrite;
513
- control.removeAttribute(this.#labelRecord);
514
- if (control.value === written) control.value = original;
515
- } else if (session.labelWrite?.channel === "text") {
516
- const { control, original, written } = session.labelWrite;
517
- control.removeAttribute(this.#labelRecord);
518
- if (control.childElementCount === 0 && control.textContent === written) {
519
- control.textContent = original;
520
- }
577
+ if (session.labelWrite) {
578
+ const { control, owner } = session.labelWrite;
579
+ owner.restore(control, { source: "live" });
580
+ owner.forget(control);
521
581
  }
522
582
  session.labelWrite = null;
523
583
  if (!unfinished) session.submitter?.removeAttribute(this.#submitterMark);