stimeo-ui 0.15.0 → 0.17.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +209 -0
  3. data/README.md +128 -1
  4. data/dist/cable/index.js +392 -417
  5. data/dist/controllers/accordion_controller.js +228 -28
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -191
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +295 -110
  10. data/dist/controllers/avatar_controller.js +172 -106
  11. data/dist/controllers/breadcrumb_controller.js +0 -129
  12. data/dist/controllers/bulk_select_controller.js +264 -53
  13. data/dist/controllers/calendar_controller.js +346 -180
  14. data/dist/controllers/carousel_controller.js +587 -300
  15. data/dist/controllers/character_counter_controller.js +271 -143
  16. data/dist/controllers/checkbox_controller.js +77 -75
  17. data/dist/controllers/clipboard_controller.js +187 -108
  18. data/dist/controllers/collapsible_controller.js +374 -108
  19. data/dist/controllers/color_picker_controller.js +183 -127
  20. data/dist/controllers/combobox_controller.js +426 -109
  21. data/dist/controllers/command_palette_controller.js +994 -350
  22. data/dist/controllers/conditional_fields_controller.js +212 -125
  23. data/dist/controllers/confirm_controller.js +939 -245
  24. data/dist/controllers/context_menu_controller.js +275 -127
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +235 -97
  27. data/dist/controllers/currency_input_controller.js +241 -118
  28. data/dist/controllers/data_grid_controller.js +229 -135
  29. data/dist/controllers/date_range_picker_controller.js +359 -165
  30. data/dist/controllers/dialog_controller.js +887 -225
  31. data/dist/controllers/direct_upload_controller.js +80 -142
  32. data/dist/controllers/dirty_form_controller.js +26 -47
  33. data/dist/controllers/dismissible_controller.js +175 -20
  34. data/dist/controllers/drawer_controller.js +963 -336
  35. data/dist/controllers/dropdown_controller.js +342 -85
  36. data/dist/controllers/editable_controller.js +182 -58
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +426 -229
  39. data/dist/controllers/filter_controller.js +78 -36
  40. data/dist/controllers/flash_controller.js +328 -211
  41. data/dist/controllers/focus_controller.js +654 -239
  42. data/dist/controllers/form_field_controller.js +153 -136
  43. data/dist/controllers/form_validation_controller.js +29 -96
  44. data/dist/controllers/frame_loading_controller.js +263 -198
  45. data/dist/controllers/highlight_controller.js +119 -70
  46. data/dist/controllers/hover_card_controller.js +335 -118
  47. data/dist/controllers/idle_controller.js +316 -65
  48. data/dist/controllers/input_mask_controller.js +116 -61
  49. data/dist/controllers/intersection_controller.js +147 -103
  50. data/dist/controllers/lazy_frame_controller.js +9 -56
  51. data/dist/controllers/listbox_controller.js +413 -153
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -95
  54. data/dist/controllers/menu_controller.js +254 -167
  55. data/dist/controllers/menubar_controller.js +206 -331
  56. data/dist/controllers/meter_controller.js +159 -71
  57. data/dist/controllers/multi_select_controller.js +797 -388
  58. data/dist/controllers/navigation_menu_controller.js +156 -231
  59. data/dist/controllers/nested_form_controller.js +373 -174
  60. data/dist/controllers/network_status_controller.js +241 -45
  61. data/dist/controllers/number_input_controller.js +387 -227
  62. data/dist/controllers/optimistic_controller.js +28 -75
  63. data/dist/controllers/otp_controller.js +344 -196
  64. data/dist/controllers/overflow_indicator_controller.js +167 -93
  65. data/dist/controllers/overflow_menu_controller.js +408 -277
  66. data/dist/controllers/pagination_controller.js +342 -120
  67. data/dist/controllers/password_reveal_controller.js +424 -99
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +252 -96
  70. data/dist/controllers/pointer_drag_controller.js +263 -164
  71. data/dist/controllers/popover_controller.js +267 -94
  72. data/dist/controllers/portal_controller.js +209 -72
  73. data/dist/controllers/preview_guard_controller.js +243 -112
  74. data/dist/controllers/progress_controller.js +132 -59
  75. data/dist/controllers/radio_group_controller.js +138 -133
  76. data/dist/controllers/range_slider_controller.js +245 -112
  77. data/dist/controllers/rating_controller.js +429 -194
  78. data/dist/controllers/read_more_controller.js +185 -47
  79. data/dist/controllers/reading_progress_controller.js +147 -135
  80. data/dist/controllers/relative_time_controller.js +145 -110
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  82. data/dist/controllers/resizable_controller.js +294 -118
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -221
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -146
  87. data/dist/controllers/scrollspy_controller.js +181 -236
  88. data/dist/controllers/separator_controller.js +288 -132
  89. data/dist/controllers/sidebar_controller.js +1071 -375
  90. data/dist/controllers/skeleton_controller.js +248 -92
  91. data/dist/controllers/slider_controller.js +211 -96
  92. data/dist/controllers/smart_sticky_header_controller.js +163 -54
  93. data/dist/controllers/sortable_controller.js +13 -110
  94. data/dist/controllers/spinner_controller.js +277 -161
  95. data/dist/controllers/step_indicator_controller.js +107 -53
  96. data/dist/controllers/stepper_controller.js +171 -48
  97. data/dist/controllers/stick_to_bottom_controller.js +310 -116
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -192
  100. data/dist/controllers/switch_controller.js +107 -43
  101. data/dist/controllers/tabs_controller.js +50 -25
  102. data/dist/controllers/tags_input_controller.js +290 -149
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +148 -80
  105. data/dist/controllers/time_picker_controller.js +162 -81
  106. data/dist/controllers/toast_controller.js +365 -193
  107. data/dist/controllers/toggle_group_controller.js +194 -99
  108. data/dist/controllers/toolbar_controller.js +92 -90
  109. data/dist/controllers/tooltip_controller.js +185 -125
  110. data/dist/controllers/transition_controller.js +123 -115
  111. data/dist/controllers/tree_view_controller.js +248 -290
  112. data/dist/index.js +8649 -8572
  113. data/dist/positioning/index.js +148 -45
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,55 +1,71 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/input_mask_controller.ts
4
-
5
- // src/utils/composition_tracker.ts
3
+ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
4
+ "insertCompositionText",
5
+ "insertFromComposition",
6
+ "deleteCompositionText",
7
+ "deleteByComposition"
8
+ ]);
6
9
  var CompositionTracker = class {
7
10
  #observedTargets = /* @__PURE__ */ new Set();
8
11
  #activeTargets = /* @__PURE__ */ new Set();
9
12
  #onStart;
10
13
  #onEnd;
14
+ #confirmedTarget = null;
11
15
  constructor(options = {}) {
12
16
  this.#onStart = options.onStart;
13
17
  this.#onEnd = options.onEnd;
14
18
  }
15
- /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
16
19
  observe(target) {
17
20
  if (this.#observedTargets.has(target)) return;
18
21
  target.addEventListener("compositionstart", this.#handleStart);
19
22
  target.addEventListener("compositionend", this.#handleEnd);
23
+ target.addEventListener("keydown", this.#handleKeydown);
20
24
  this.#observedTargets.add(target);
21
25
  }
22
- /** Stops tracking one target and clears any active composition it owned. */
23
26
  unobserve(target) {
24
27
  if (!this.#observedTargets.delete(target)) return;
25
28
  target.removeEventListener("compositionstart", this.#handleStart);
26
29
  target.removeEventListener("compositionend", this.#handleEnd);
30
+ target.removeEventListener("keydown", this.#handleKeydown);
27
31
  this.#activeTargets.delete(target);
32
+ if (this.#confirmedTarget === target) this.#confirmedTarget = null;
28
33
  }
29
- /** Releases every listener and clears state so reconnect starts cleanly. */
30
34
  disconnect() {
31
35
  for (const target of this.#observedTargets) {
32
36
  target.removeEventListener("compositionstart", this.#handleStart);
33
37
  target.removeEventListener("compositionend", this.#handleEnd);
38
+ target.removeEventListener("keydown", this.#handleKeydown);
34
39
  }
35
40
  this.#observedTargets.clear();
36
41
  this.#activeTargets.clear();
42
+ this.#confirmedTarget = null;
37
43
  }
38
- /** True when lifecycle tracking or the current event reports composition. */
39
44
  isComposing(event) {
40
45
  return this.#activeTargets.size > 0 || event?.isComposing === true;
41
46
  }
47
+ consumesConfirmedInput(event) {
48
+ const confirmed = this.#confirmedTarget;
49
+ this.#confirmedTarget = null;
50
+ if (confirmed === null || confirmed !== event.target) return false;
51
+ const inputType = event.inputType;
52
+ return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
53
+ }
42
54
  #handleStart = (event) => {
55
+ this.#confirmedTarget = null;
43
56
  if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
44
57
  this.#onStart?.(event);
45
58
  };
46
59
  #handleEnd = (event) => {
47
60
  if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
61
+ this.#confirmedTarget = event.target;
48
62
  this.#onEnd?.(event);
49
63
  };
64
+ #handleKeydown = () => {
65
+ this.#confirmedTarget = null;
66
+ };
50
67
  };
51
68
 
52
- // src/utils/declared_value.ts
53
69
  function parseDeclared(raw, parse, fallback) {
54
70
  try {
55
71
  return parse(raw);
@@ -73,7 +89,6 @@ function parseJsonObject(raw) {
73
89
  return parsed;
74
90
  }
75
91
 
76
- // src/utils/focus_candidate.ts
77
92
  function inheritsFieldsetDisabled(control) {
78
93
  let fieldset = control.closest("fieldset[disabled]");
79
94
  while (fieldset) {
@@ -84,7 +99,6 @@ function inheritsFieldsetDisabled(control) {
84
99
  return false;
85
100
  }
86
101
 
87
- // src/utils/half_width.ts
88
102
  var FULL_WIDTH_SHIFT = 65248;
89
103
  function halfWidthChar(char) {
90
104
  if (char >= "\uFF01" && char <= "\uFF5E") {
@@ -93,13 +107,76 @@ function halfWidthChar(char) {
93
107
  return char === "\u3000" ? " " : char;
94
108
  }
95
109
 
96
- // src/controllers/input_mask_controller.ts
110
+ var ListenerSet = class {
111
+ #abort = new AbortController();
112
+ add(target, type, handler, options) {
113
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
114
+ }
115
+ dispose() {
116
+ this.#abort.abort();
117
+ this.#abort = new AbortController();
118
+ }
119
+ };
120
+
121
+ var MicrotaskCoalescer = class {
122
+ #run;
123
+ #queued = false;
124
+ #active = false;
125
+ #generation = 0;
126
+ constructor(run) {
127
+ this.#run = run;
128
+ }
129
+ activate() {
130
+ this.#active = true;
131
+ }
132
+ cancel() {
133
+ this.#active = false;
134
+ this.#queued = false;
135
+ this.#generation += 1;
136
+ }
137
+ schedule() {
138
+ if (!this.#active || this.#queued) return;
139
+ this.#queued = true;
140
+ const generation = this.#generation;
141
+ queueMicrotask(() => {
142
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
143
+ this.#queued = false;
144
+ this.#run();
145
+ });
146
+ }
147
+ };
148
+
149
+ var MorphRenderWatcher = class {
150
+ #listeners = new ListenerSet();
151
+ #pass;
152
+ #element = null;
153
+ #onMorph = () => this.#pass.schedule();
154
+ constructor(run) {
155
+ this.#pass = new MicrotaskCoalescer(run);
156
+ }
157
+ observe(element) {
158
+ if (this.#element === element) return;
159
+ this.disconnect();
160
+ this.#element = element;
161
+ this.#pass.activate();
162
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
163
+ }
164
+ schedule() {
165
+ this.#pass.schedule();
166
+ }
167
+ disconnect() {
168
+ this.#listeners.dispose();
169
+ this.#pass.cancel();
170
+ this.#element = null;
171
+ }
172
+ };
173
+
97
174
  var DEFAULT_TOKENS = {
98
175
  "9": "\\d",
99
176
  a: "[A-Za-z]",
100
177
  "*": "[A-Za-z0-9]"
101
178
  };
102
- var UNMASK_ATTR = "data-stimeo--input-mask-unmask";
179
+ var UNMASK_ATTR = "unmask";
103
180
  function acceptedForm(regex, char) {
104
181
  if (regex.test(char)) return char;
105
182
  const half = halfWidthChar(char);
@@ -176,6 +253,9 @@ function tokenCharOffset(tokenFlags, n) {
176
253
  return -1;
177
254
  }
178
255
  var InputMaskController = class extends Controller {
256
+ get #unmaskAttribute() {
257
+ return `data-${this.identifier}-${UNMASK_ATTR}`;
258
+ }
179
259
  static values = {
180
260
  pattern: { type: String, default: "" },
181
261
  tokens: { type: String, default: "" },
@@ -183,14 +263,19 @@ var InputMaskController = class extends Controller {
183
263
  };
184
264
  static actions = ["format"];
185
265
  static events = ["change", "reconcile"];
186
- /** The value this controller last committed, and the baseline both events compare. */
187
266
  #lastValue = null;
188
267
  #started = false;
189
- /** Validated token map; the hot path never parses the `tokens` declaration. */
190
268
  #tokens = compileTokens("");
191
- /** Holds mid-composition input so the IME's uncommitted text is never rewritten. */
192
- #composition = new CompositionTracker({ onEnd: () => this.#reformat("edit") });
193
- /** Re-parses the declaration and re-formats under the tokens it now selects. */
269
+ #textBeforeComposition = "";
270
+ #composition = new CompositionTracker({
271
+ onStart: () => {
272
+ this.#textBeforeComposition = this.element.value;
273
+ },
274
+ onEnd: () => {
275
+ const committedNothing = this.element.value === this.#textBeforeComposition;
276
+ this.#reformat(committedNothing ? "reconcile" : "edit");
277
+ }
278
+ });
194
279
  tokensValueChanged() {
195
280
  this.#tokens = compileTokens(this.tokensValue);
196
281
  if (this.#started) this.#reformat("reconcile");
@@ -198,7 +283,6 @@ var InputMaskController = class extends Controller {
198
283
  patternValueChanged() {
199
284
  if (this.#started) this.#reformat("reconcile");
200
285
  }
201
- /** Clears a sink it stops maintaining so a submit cannot carry a stale raw value. */
202
286
  unmaskToHiddenValueChanged() {
203
287
  if (!this.#started) return;
204
288
  if (!this.unmaskToHiddenValue) {
@@ -207,26 +291,25 @@ var InputMaskController = class extends Controller {
207
291
  }
208
292
  this.#reformat("reconcile");
209
293
  }
294
+ #morph = new MorphRenderWatcher(() => this.#reformat("reconcile"));
210
295
  connect() {
296
+ this.#morph.observe(this.element);
211
297
  this.#started = true;
212
298
  this.#composition.observe(this.element);
213
299
  this.#lastValue = this.element.value;
214
300
  this.#reformat("reconcile");
215
301
  }
216
302
  disconnect() {
303
+ this.#morph.disconnect();
217
304
  this.#started = false;
218
305
  this.#composition.disconnect();
219
306
  }
220
- /** Formats the field on input, preserving the caret. Bound via `data-action`. */
221
307
  format(event) {
222
308
  if (this.#composition.isComposing(event)) return;
223
309
  this.#reformat("edit", deletionOf(event));
224
310
  }
225
- /**
226
- * Core reformat: mask the current value, restore the caret, sync the sink and
227
- * the state hooks, and report a moved value under the event `cause` selects.
228
- */
229
311
  #reformat(cause, deletion = null) {
312
+ if (this.#composition.isComposing()) return;
230
313
  const input = this.element;
231
314
  const raw = input.value;
232
315
  if (this.patternValue === "") {
@@ -255,8 +338,10 @@ var InputMaskController = class extends Controller {
255
338
  this.#lastValue = raw;
256
339
  return;
257
340
  }
258
- input.value = result.masked;
259
- this.#restoreCaret(result.tokenFlags, anchor);
341
+ if (input.value !== result.masked) {
342
+ input.value = result.masked;
343
+ this.#restoreCaret(result.tokenFlags, anchor);
344
+ }
260
345
  this.#publish(result, result.masked);
261
346
  if (result.masked === this.#lastValue) return;
262
347
  this.#lastValue = result.masked;
@@ -264,7 +349,6 @@ var InputMaskController = class extends Controller {
264
349
  if (cause === "edit") this.dispatch("change", { detail });
265
350
  else this.dispatch("reconcile", { detail });
266
351
  }
267
- /** Syncs the raw-value sink and the state hooks for the `shown` field text. */
268
352
  #publish(result, shown) {
269
353
  if (this.unmaskToHiddenValue) {
270
354
  const sink = this.#resolveSink();
@@ -273,15 +357,9 @@ var InputMaskController = class extends Controller {
273
357
  this.#flag("data-mask-complete", result.complete);
274
358
  this.#flag("data-mask-empty", shown.length === 0);
275
359
  }
276
- /**
277
- * How many token slots the text before the caret fills. Masking that prefix is
278
- * what makes a rejected character — or a literal that also matches a token —
279
- * count exactly as the rendering counts it, so the caret cannot drift.
280
- */
281
360
  #significantBefore(prefix) {
282
361
  return applyMask(prefix, this.patternValue, this.#tokens).unmasked.length;
283
362
  }
284
- /** Places the caret after the `n`-th token char (skipping following literals). */
285
363
  #restoreCaret(tokenFlags, n) {
286
364
  let position;
287
365
  if (n <= 0) {
@@ -303,31 +381,14 @@ var InputMaskController = class extends Controller {
303
381
  } catch {
304
382
  }
305
383
  }
306
- /**
307
- * The hidden raw-value sink for this input, resolved so several masked inputs
308
- * can coexist in one form:
309
- *
310
- * 1. **Explicit pairing** — a sink whose attribute value names this input's
311
- * `id` (`data-stimeo--input-mask-unmask="zip"`).
312
- * 2. **Nearest container** — otherwise, walking up from the input, the first
313
- * *value-less* sink in the closest ancestor. Wrapped input+sink pairs each
314
- * find their own sink, and the single form-level sink keeps working
315
- * unchanged. A sink claimed by another input's id is never matched here.
316
- *
317
- * Both steps only ever consider the associated form's own controls, so a sink
318
- * belonging to a different form (or to none) is never written, while one the
319
- * `form` attribute associates from elsewhere in the document still resolves —
320
- * for either the input or the sink, neither of which needs to contain or be
321
- * contained by the form. A form-less input reads the document instead.
322
- */
323
384
  #resolveSink() {
324
385
  const candidates = this.#sinkCandidates();
325
386
  const id = this.element.id;
326
387
  if (id.length > 0) {
327
- const paired = candidates.find((sink) => sink.getAttribute(UNMASK_ATTR) === id);
388
+ const paired = candidates.find((sink) => sink.getAttribute(this.#unmaskAttribute) === id);
328
389
  if (paired) return paired;
329
390
  }
330
- const free = candidates.filter((sink) => sink.getAttribute(UNMASK_ATTR) === "");
391
+ const free = candidates.filter((sink) => sink.getAttribute(this.#unmaskAttribute) === "");
331
392
  for (let node = this.element.parentElement; node !== null; node = node.parentElement) {
332
393
  const ancestor = node;
333
394
  const sink = free.find((candidate) => ancestor.contains(candidate));
@@ -335,22 +396,17 @@ var InputMaskController = class extends Controller {
335
396
  }
336
397
  return null;
337
398
  }
338
- /**
339
- * Every sink the input's form owns, in document order — one collection read per
340
- * resolution, so the cost tracks the form's own controls and not the depth of
341
- * the markup around it. `form.elements` lists controls the `form` attribute
342
- * associates from anywhere in the document, and lists nothing another form owns.
343
- */
344
399
  #sinkCandidates() {
345
400
  const form = this.element.form;
346
401
  if (form === null) {
347
- return Array.from(document.querySelectorAll(`input[${UNMASK_ATTR}]`));
402
+ return Array.from(
403
+ document.querySelectorAll(`input[${this.#unmaskAttribute}]`)
404
+ );
348
405
  }
349
406
  return Array.from(form.elements).filter(
350
- (element) => element instanceof HTMLInputElement && element.hasAttribute(UNMASK_ATTR)
407
+ (element) => element instanceof HTMLInputElement && element.hasAttribute(this.#unmaskAttribute)
351
408
  );
352
409
  }
353
- /** Sets a boolean `data-*` flag to `"true"` when `on`, else removes it. */
354
410
  #flag(name, on) {
355
411
  if (on) this.element.setAttribute(name, "true");
356
412
  else this.element.removeAttribute(name);
@@ -358,5 +414,4 @@ var InputMaskController = class extends Controller {
358
414
  };
359
415
 
360
416
  export { InputMaskController, applyMask };
361
- //# sourceMappingURL=input_mask_controller.js.map
362
417
  //# sourceMappingURL=input_mask_controller.js.map