stimeo-ui 0.16.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +136 -0
  3. data/README.md +27 -11
  4. data/dist/cable/index.js +384 -449
  5. data/dist/controllers/accordion_controller.js +174 -55
  6. data/dist/controllers/alert_dialog_controller.js +903 -235
  7. data/dist/controllers/announcer_controller.js +238 -205
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +414 -145
  10. data/dist/controllers/avatar_controller.js +185 -107
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +282 -58
  13. data/dist/controllers/calendar_controller.js +228 -260
  14. data/dist/controllers/carousel_controller.js +536 -436
  15. data/dist/controllers/character_counter_controller.js +493 -207
  16. data/dist/controllers/checkbox_controller.js +38 -74
  17. data/dist/controllers/clipboard_controller.js +261 -149
  18. data/dist/controllers/collapsible_controller.js +304 -120
  19. data/dist/controllers/color_picker_controller.js +178 -146
  20. data/dist/controllers/combobox_controller.js +522 -156
  21. data/dist/controllers/command_palette_controller.js +1156 -395
  22. data/dist/controllers/conditional_fields_controller.js +300 -171
  23. data/dist/controllers/confirm_controller.js +980 -262
  24. data/dist/controllers/context_menu_controller.js +346 -153
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +281 -158
  27. data/dist/controllers/currency_input_controller.js +575 -225
  28. data/dist/controllers/data_grid_controller.js +248 -214
  29. data/dist/controllers/date_range_picker_controller.js +338 -235
  30. data/dist/controllers/dialog_controller.js +899 -237
  31. data/dist/controllers/direct_upload_controller.js +97 -167
  32. data/dist/controllers/dirty_form_controller.js +72 -69
  33. data/dist/controllers/dismissible_controller.js +196 -24
  34. data/dist/controllers/drawer_controller.js +974 -355
  35. data/dist/controllers/dropdown_controller.js +339 -96
  36. data/dist/controllers/editable_controller.js +315 -88
  37. data/dist/controllers/empty_state_controller.js +79 -58
  38. data/dist/controllers/file_dropzone_controller.js +322 -211
  39. data/dist/controllers/filter_controller.js +77 -42
  40. data/dist/controllers/flash_controller.js +238 -347
  41. data/dist/controllers/focus_controller.js +679 -240
  42. data/dist/controllers/form_field_controller.js +156 -132
  43. data/dist/controllers/form_validation_controller.js +31 -97
  44. data/dist/controllers/frame_loading_controller.js +275 -217
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +327 -130
  47. data/dist/controllers/idle_controller.js +271 -101
  48. data/dist/controllers/input_mask_controller.js +483 -97
  49. data/dist/controllers/intersection_controller.js +150 -106
  50. data/dist/controllers/lazy_frame_controller.js +24 -68
  51. data/dist/controllers/listbox_controller.js +523 -240
  52. data/dist/controllers/local_time_controller.js +102 -77
  53. data/dist/controllers/masonry_controller.js +80 -96
  54. data/dist/controllers/menu_controller.js +302 -188
  55. data/dist/controllers/menubar_controller.js +232 -345
  56. data/dist/controllers/meter_controller.js +417 -87
  57. data/dist/controllers/multi_select_controller.js +785 -393
  58. data/dist/controllers/navigation_menu_controller.js +190 -250
  59. data/dist/controllers/nested_form_controller.js +396 -192
  60. data/dist/controllers/network_status_controller.js +268 -62
  61. data/dist/controllers/number_input_controller.js +610 -303
  62. data/dist/controllers/optimistic_controller.js +45 -87
  63. data/dist/controllers/otp_controller.js +658 -255
  64. data/dist/controllers/overflow_indicator_controller.js +122 -113
  65. data/dist/controllers/overflow_menu_controller.js +470 -295
  66. data/dist/controllers/pagination_controller.js +332 -123
  67. data/dist/controllers/password_reveal_controller.js +392 -121
  68. data/dist/controllers/password_strength_controller.js +111 -158
  69. data/dist/controllers/persist_controller.js +165 -111
  70. data/dist/controllers/pointer_drag_controller.js +434 -188
  71. data/dist/controllers/popover_controller.js +269 -113
  72. data/dist/controllers/portal_controller.js +215 -76
  73. data/dist/controllers/preview_guard_controller.js +243 -116
  74. data/dist/controllers/progress_controller.js +391 -78
  75. data/dist/controllers/radio_group_controller.js +278 -211
  76. data/dist/controllers/range_slider_controller.js +220 -113
  77. data/dist/controllers/rating_controller.js +458 -211
  78. data/dist/controllers/read_more_controller.js +138 -63
  79. data/dist/controllers/reading_progress_controller.js +166 -137
  80. data/dist/controllers/relative_time_controller.js +211 -112
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  82. data/dist/controllers/resizable_controller.js +299 -150
  83. data/dist/controllers/roving_controller.js +49 -88
  84. data/dist/controllers/scroll_area_controller.js +490 -246
  85. data/dist/controllers/scroll_restore_controller.js +14 -55
  86. data/dist/controllers/scroll_visibility_controller.js +432 -149
  87. data/dist/controllers/scrollspy_controller.js +193 -248
  88. data/dist/controllers/separator_controller.js +285 -142
  89. data/dist/controllers/sidebar_controller.js +1069 -404
  90. data/dist/controllers/skeleton_controller.js +259 -101
  91. data/dist/controllers/slider_controller.js +187 -86
  92. data/dist/controllers/smart_sticky_header_controller.js +173 -66
  93. data/dist/controllers/sortable_controller.js +26 -125
  94. data/dist/controllers/spinner_controller.js +288 -173
  95. data/dist/controllers/step_indicator_controller.js +105 -46
  96. data/dist/controllers/stepper_controller.js +120 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +275 -140
  98. data/dist/controllers/sticky_observer_controller.js +220 -49
  99. data/dist/controllers/submit_once_controller.js +376 -227
  100. data/dist/controllers/switch_controller.js +201 -84
  101. data/dist/controllers/tabs_controller.js +41 -30
  102. data/dist/controllers/tags_input_controller.js +293 -173
  103. data/dist/controllers/textarea_autosize_controller.js +108 -74
  104. data/dist/controllers/theme_controller.js +140 -116
  105. data/dist/controllers/time_picker_controller.js +331 -127
  106. data/dist/controllers/toast_controller.js +244 -324
  107. data/dist/controllers/toggle_group_controller.js +252 -191
  108. data/dist/controllers/toolbar_controller.js +219 -120
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +283 -369
  112. data/dist/index.js +8573 -10410
  113. data/dist/positioning/index.js +154 -47
  114. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  115. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  116. data/lib/stimeo/ui/version.rb +1 -1
  117. metadata +3 -3
@@ -1,8 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/smart_sticky_header_controller.ts
4
-
5
- // src/utils/declared_value.ts
6
3
  function parseDeclared(raw, parse, fallback) {
7
4
  try {
8
5
  return parse(raw);
@@ -22,14 +19,8 @@ function validSelector(element, raw, fallback) {
22
19
  );
23
20
  }
24
21
 
25
- // src/utils/frame_coalescer.ts
26
22
  var FrameCoalescer = class {
27
23
  #frame = null;
28
- /**
29
- * Runs `run` on the next frame, unless a frame is already pending — the first
30
- * request of a burst wins and the rest are dropped. The pending frame is
31
- * released before `run`, so `run` may request the next one.
32
- */
33
24
  schedule(run) {
34
25
  if (this.#frame !== null) return;
35
26
  this.#frame = requestAnimationFrame(() => {
@@ -37,14 +28,6 @@ var FrameCoalescer = class {
37
28
  run();
38
29
  });
39
30
  }
40
- /**
41
- * Drops the pending frame, and reaches the platform only when there is one.
42
- *
43
- * There is no handle value that stands for "nothing pending":
44
- * `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
45
- * arrives as a large positive number that the same allocator can hand out, and
46
- * an idle cancel would then drop a frame belonging to someone else.
47
- */
48
31
  cancel() {
49
32
  if (this.#frame === null) return;
50
33
  cancelAnimationFrame(this.#frame);
@@ -52,7 +35,121 @@ var FrameCoalescer = class {
52
35
  }
53
36
  };
54
37
 
55
- // src/utils/scroll_source.ts
38
+ var ListenerSet = class {
39
+ #abort = new AbortController();
40
+ add(target, type, handler, options) {
41
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
42
+ }
43
+ dispose() {
44
+ this.#abort.abort();
45
+ this.#abort = new AbortController();
46
+ }
47
+ };
48
+
49
+ var MicrotaskCoalescer = class {
50
+ #run;
51
+ #queued = false;
52
+ #active = false;
53
+ #generation = 0;
54
+ constructor(run) {
55
+ this.#run = run;
56
+ }
57
+ activate() {
58
+ this.#active = true;
59
+ }
60
+ cancel() {
61
+ this.#active = false;
62
+ this.#queued = false;
63
+ this.#generation += 1;
64
+ }
65
+ get pending() {
66
+ return this.#queued;
67
+ }
68
+ schedule() {
69
+ if (!this.#active || this.#queued) return;
70
+ this.#queued = true;
71
+ const generation = this.#generation;
72
+ queueMicrotask(() => {
73
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
74
+ this.#queued = false;
75
+ this.#run();
76
+ });
77
+ }
78
+ };
79
+
80
+ var MorphRenderWatcher = class {
81
+ #listeners = new ListenerSet();
82
+ #pass;
83
+ #element = null;
84
+ #onMorph = () => this.#pass.schedule();
85
+ constructor(run) {
86
+ this.#pass = new MicrotaskCoalescer(run);
87
+ }
88
+ observe(element) {
89
+ if (this.#element === element) return;
90
+ this.disconnect();
91
+ this.#element = element;
92
+ this.#pass.activate();
93
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
94
+ }
95
+ schedule() {
96
+ this.#pass.schedule();
97
+ }
98
+ get pending() {
99
+ return this.#pass.pending;
100
+ }
101
+ disconnect() {
102
+ this.#listeners.dispose();
103
+ this.#pass.cancel();
104
+ this.#element = null;
105
+ }
106
+ };
107
+
108
+ var NUMBER_BOUNDS = {
109
+ finite: { finite: true },
110
+ nonNegative: { finite: true, min: 0 }};
111
+ function matchesNumberBounds(value, bounds) {
112
+ if (!Number.isFinite(value)) {
113
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
114
+ if (direction === null) return false;
115
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
116
+ }
117
+ if (bounds.min !== void 0 && value < bounds.min) return false;
118
+ if (bounds.max !== void 0 && value > bounds.max) return false;
119
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
120
+ if (bounds.integer && !Number.isInteger(value)) return false;
121
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
122
+ return true;
123
+ }
124
+
125
+ function readNumber(raw, fallback, bounds) {
126
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
127
+ }
128
+
129
+ var NumberValueReader = class {
130
+ #lastRejected = /* @__PURE__ */ new Map();
131
+ read(owner, name, raw, fallback, bounds) {
132
+ const resolved = readNumber(raw, fallback, bounds);
133
+ if (matchesNumberBounds(raw, bounds)) {
134
+ this.#lastRejected.delete(name);
135
+ return resolved;
136
+ }
137
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
138
+ const literal = owner.element.getAttribute(attribute);
139
+ if (literal === null) {
140
+ this.#lastRejected.delete(name);
141
+ return resolved;
142
+ }
143
+ if (this.#lastRejected.get(name) !== literal) {
144
+ this.#lastRejected.set(name, literal);
145
+ console.warn(
146
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
147
+ );
148
+ }
149
+ return resolved;
150
+ }
151
+ };
152
+
56
153
  function resolveScrollContainer(selector) {
57
154
  const match = selector ? document.querySelector(selector) : null;
58
155
  return match instanceof HTMLElement ? match : null;
@@ -64,64 +161,66 @@ function scrollOffset(source) {
64
161
  return source === window ? window.scrollY ?? window.pageYOffset ?? 0 : source.scrollTop;
65
162
  }
66
163
 
67
- // src/controllers/smart_sticky_header_controller.ts
68
164
  var DEFAULT_OFFSET = 80;
69
- var SmartStickyHeaderController = class extends Controller {
165
+ var SmartStickyHeaderController = class _SmartStickyHeaderController extends Controller {
166
+ #numbers = new NumberValueReader();
70
167
  static values = {
71
168
  containerSelector: { type: String, default: "" },
72
169
  offset: { type: Number, default: DEFAULT_OFFSET },
73
170
  tolerance: { type: Number, default: 4 }
74
171
  };
172
+ static valueConstraints = {
173
+ offset: NUMBER_BOUNDS.finite,
174
+ tolerance: NUMBER_BOUNDS.nonNegative
175
+ };
75
176
  static events = ["change"];
76
- #connected = false;
77
- /** Coalesces scroll bursts into one measurement per frame. */
78
177
  #frames = new FrameCoalescer();
79
- /** The scroll source resolved at connect — disconnect must unbind the SAME node. */
80
178
  #scrollerEl = window;
81
- /** The validated `containerSelector`, or `""` when the declaration cannot be parsed. */
179
+ #scrollListeners = new ListenerSet();
180
+ #listeners = new ListenerSet();
82
181
  #containerSelector = "";
83
182
  #lastY = 0;
84
- /** Last published state, so `change` fires only on transitions. */
85
183
  #hidden = null;
86
- /**
87
- * The depth that never hides. A declaration that is not a finite number reads
88
- * as the default, so the comparison path never sees `NaN` — which would
89
- * answer `false` to every comparison and hide the header inside the very
90
- * zone the value exists to protect.
91
- */
92
184
  get #offset() {
93
- return Number.isFinite(this.offsetValue) ? this.offsetValue : DEFAULT_OFFSET;
185
+ return this.#safeOffset;
94
186
  }
95
187
  #onScroll = () => this.#frames.schedule(() => this.#measure());
96
- /**
97
- * Focus inside a hidden header must reveal it (WCAG 2.4.7 / 2.4.11); the
98
- * hold while focus *stays* inside is the `#apply` hide invariant.
99
- */
100
188
  #onFocusin = () => this.#apply(false);
101
- /** Validates `containerSelector` once so connect never parses a selector that throws. */
102
189
  containerSelectorValueChanged() {
103
190
  this.#containerSelector = validSelector(this.element, this.containerSelectorValue, "");
191
+ this.#morphRender.schedule();
104
192
  }
105
- /** Re-decides when application code (or a Turbo morph) changes `offset` at runtime. */
106
193
  offsetValueChanged() {
107
- if (this.#connected) this.#measure();
194
+ this.#morphRender.schedule();
108
195
  }
196
+ #morphRender = new MorphRenderWatcher(() => {
197
+ this.#followSource();
198
+ this.#measure();
199
+ });
109
200
  connect() {
201
+ this.#morphRender.observe(this.element);
110
202
  this.#hidden = null;
111
- this.#scrollerEl = resolveScrollSource(this.#containerSelector);
112
- this.#lastY = scrollOffset(this.#scrollerEl);
113
- this.#scrollerEl.addEventListener("scroll", this.#onScroll, { passive: true });
114
- this.element.addEventListener("focusin", this.#onFocusin);
203
+ this.#listenTo(resolveScrollSource(this.#containerSelector));
204
+ this.#listeners.add(this.element, "focusin", this.#onFocusin);
115
205
  this.#apply(false, false);
116
- this.#connected = true;
117
206
  }
118
207
  disconnect() {
119
- this.#connected = false;
120
- this.#scrollerEl.removeEventListener("scroll", this.#onScroll);
121
- this.element.removeEventListener("focusin", this.#onFocusin);
208
+ this.#morphRender.disconnect();
209
+ this.#scrollListeners.dispose();
210
+ this.#listeners.dispose();
122
211
  this.#frames.cancel();
123
212
  }
124
- /** @stimeoRenderRoot */
213
+ #followSource() {
214
+ const source = resolveScrollSource(this.#containerSelector);
215
+ if (source === this.#scrollerEl) return;
216
+ this.#scrollListeners.dispose();
217
+ this.#listenTo(source);
218
+ }
219
+ #listenTo(source) {
220
+ this.#scrollerEl = source;
221
+ this.#lastY = scrollOffset(source);
222
+ this.#scrollListeners.add(source, "scroll", this.#onScroll, { passive: true });
223
+ }
125
224
  #measure() {
126
225
  const y = scrollOffset(this.#scrollerEl);
127
226
  if (y <= this.#offset) {
@@ -130,34 +229,42 @@ var SmartStickyHeaderController = class extends Controller {
130
229
  return;
131
230
  }
132
231
  const delta = y - this.#lastY;
133
- if (this.#isJitter(delta)) return;
232
+ if (this.#isJitter(delta)) {
233
+ this.#apply(this.#hidden ?? false);
234
+ return;
235
+ }
134
236
  this.#lastY = y;
135
237
  this.#apply(delta > 0);
136
238
  }
137
- /**
138
- * Whether one scroll step is too small to act on.
139
- *
140
- * @stimeoRuntimeOnly `tolerance` is the allowance one scroll step is compared against; the
141
- * header's state at rest does not depend on it.
142
- */
143
239
  #isJitter(delta) {
144
- return Math.abs(delta) < this.toleranceValue;
145
- }
146
- /**
147
- * Reflects the state onto the hook and emits `change` on transitions.
148
- *
149
- * @param notify - whether a transition announces itself. The reflection
150
- * `connect()` performs is the current state, not a change.
151
- */
240
+ return Math.abs(delta) < this.#safeTolerance;
241
+ }
152
242
  #apply(hidden, notify = true) {
153
243
  if (hidden && this.element.contains(document.activeElement)) return;
154
- if (hidden === this.#hidden) return;
244
+ const changed = hidden !== this.#hidden;
155
245
  this.#hidden = hidden;
156
246
  this.element.setAttribute("data-header-hidden", hidden ? "true" : "false");
157
- if (notify) this.dispatch("change", { detail: { hidden } });
247
+ if (notify && changed) this.dispatch("change", { detail: { hidden } });
248
+ }
249
+ get #safeOffset() {
250
+ return this.#numbers.read(
251
+ this,
252
+ "offset",
253
+ this.offsetValue,
254
+ _SmartStickyHeaderController.values.offset.default,
255
+ _SmartStickyHeaderController.valueConstraints.offset
256
+ );
257
+ }
258
+ get #safeTolerance() {
259
+ return this.#numbers.read(
260
+ this,
261
+ "tolerance",
262
+ this.toleranceValue,
263
+ _SmartStickyHeaderController.values.tolerance.default,
264
+ _SmartStickyHeaderController.valueConstraints.tolerance
265
+ );
158
266
  }
159
267
  };
160
268
 
161
269
  export { SmartStickyHeaderController };
162
- //# sourceMappingURL=smart_sticky_header_controller.js.map
163
270
  //# sourceMappingURL=smart_sticky_header_controller.js.map
@@ -1,8 +1,12 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/sortable_controller.ts
3
+ function fillTemplate(template, values) {
4
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
5
+ const replacement = Object.hasOwn(values, name) ? values[name] : void 0;
6
+ return replacement === void 0 ? match : String(replacement);
7
+ });
8
+ }
4
9
 
5
- // src/utils/announce.ts
6
10
  function announce(message, options = {}) {
7
11
  const text = message.trim();
8
12
  if (text.length === 0) return;
@@ -12,39 +16,41 @@ function announce(message, options = {}) {
12
16
  })
13
17
  );
14
18
  }
15
- function fillTemplate(template, values) {
16
- return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
17
- const replacement = values[name];
18
- return replacement === void 0 ? match : String(replacement);
19
- });
20
- }
21
19
 
22
- // src/utils/logical_scroll.ts
20
+ var ListenerSet = class {
21
+ #abort = new AbortController();
22
+ add(target, type, handler, options) {
23
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
24
+ }
25
+ dispose() {
26
+ this.#abort.abort();
27
+ this.#abort = new AbortController();
28
+ }
29
+ };
30
+
23
31
  function isRtl(element) {
24
32
  return window.getComputedStyle(element).direction === "rtl";
25
33
  }
26
34
 
27
- // src/utils/microtask_coalescer.ts
28
35
  var MicrotaskCoalescer = class {
29
36
  #run;
30
37
  #queued = false;
31
38
  #active = false;
32
39
  #generation = 0;
33
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
34
40
  constructor(run) {
35
41
  this.#run = run;
36
42
  }
37
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
38
43
  activate() {
39
44
  this.#active = true;
40
45
  }
41
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
42
46
  cancel() {
43
47
  this.#active = false;
44
48
  this.#queued = false;
45
49
  this.#generation += 1;
46
50
  }
47
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
51
+ get pending() {
52
+ return this.#queued;
53
+ }
48
54
  schedule() {
49
55
  if (!this.#active || this.#queued) return;
50
56
  this.#queued = true;
@@ -57,19 +63,16 @@ var MicrotaskCoalescer = class {
57
63
  }
58
64
  };
59
65
 
60
- // src/utils/transient_hooks.ts
61
66
  var TransientHooks = class {
62
67
  #attributes;
63
68
  constructor(options) {
64
69
  this.#attributes = options.attributes;
65
70
  }
66
- /** Drops the declared hooks from `element`; what it carries besides stays. */
67
71
  reset(element) {
68
72
  for (const attribute of this.#attributes) element.removeAttribute(attribute);
69
73
  }
70
74
  };
71
75
 
72
- // src/controllers/sortable_controller.ts
73
76
  var TRANSIENT = new TransientHooks({ attributes: ["data-sortable-dragging"] });
74
77
  var SortableController = class extends Controller {
75
78
  static targets = ["list", "item"];
@@ -82,36 +85,22 @@ var SortableController = class extends Controller {
82
85
  };
83
86
  static events = ["reorder"];
84
87
  #session = null;
85
- /** Defers the lost-item check to after the mutation batch (see below). */
86
88
  #settle = new MicrotaskCoalescer(() => this.#dropLostSession());
89
+ #listeners = new ListenerSet();
87
90
  connect() {
88
91
  TRANSIENT.reset(this.element);
89
92
  this.#settle.activate();
90
- this.element.addEventListener("stimeo--pointer-drag:start", this.#onDragStart);
91
- this.element.addEventListener("stimeo--pointer-drag:move", this.#onDragMove);
92
- this.element.addEventListener("stimeo--pointer-drag:end", this.#onDragEnd);
93
- this.element.addEventListener("stimeo--pointer-drag:cancel", this.#onDragCancel);
93
+ this.#listeners.add(this.element, "stimeo--pointer-drag:start", this.#onDragStart);
94
+ this.#listeners.add(this.element, "stimeo--pointer-drag:move", this.#onDragMove);
95
+ this.#listeners.add(this.element, "stimeo--pointer-drag:end", this.#onDragEnd);
96
+ this.#listeners.add(this.element, "stimeo--pointer-drag:cancel", this.#onDragCancel);
94
97
  }
95
98
  disconnect() {
96
- this.element.removeEventListener("stimeo--pointer-drag:start", this.#onDragStart);
97
- this.element.removeEventListener("stimeo--pointer-drag:move", this.#onDragMove);
98
- this.element.removeEventListener("stimeo--pointer-drag:end", this.#onDragEnd);
99
- this.element.removeEventListener("stimeo--pointer-drag:cancel", this.#onDragCancel);
99
+ this.#listeners.dispose();
100
100
  this.#settle.cancel();
101
101
  this.#session = null;
102
102
  this.element.removeAttribute("data-sortable-dragging");
103
103
  }
104
- /**
105
- * Ends a session whose item left the item set.
106
- *
107
- * The controller's own reorder detaches and reattaches the item inside one
108
- * mutation batch, so the loss is only real once the batch has settled — the
109
- * check therefore runs a microtask later and asks whether the item is a target
110
- * again. Without it a deleted row (a broadcast that drops it from the board)
111
- * or a morph that strips the item's target attribute would hold the
112
- * one-at-a-time session for the rest of the page's life, with the root hook
113
- * stuck on and every later grab refused with no way out.
114
- */
115
104
  itemTargetDisconnected(item) {
116
105
  if (this.#session?.item !== item) return;
117
106
  this.#settle.schedule();
@@ -125,7 +114,6 @@ var SortableController = class extends Controller {
125
114
  this.element.removeAttribute("data-sortable-dragging");
126
115
  if (session.item.isConnected) this.#restore(session, items);
127
116
  }
128
- /** Picks the item up: remembers its neighbours and announces the grab. */
129
117
  #onDragStart = (event) => {
130
118
  if (this.#session) return;
131
119
  const items = this.#items();
@@ -153,7 +141,6 @@ var SortableController = class extends Controller {
153
141
  this.#followPointer(session, detail, items);
154
142
  }
155
143
  };
156
- /** Drops the item: announces, then reports `reorder` if the position changed. */
157
144
  #onDragEnd = (event) => {
158
145
  const session = this.#session;
159
146
  if (!session) return;
@@ -168,7 +155,6 @@ var SortableController = class extends Controller {
168
155
  this.dispatch("reorder", { detail: { item: session.item, from, to } });
169
156
  }
170
157
  };
171
- /** Restores the pickup position (Escape / OS `pointercancel`). */
172
158
  #onDragCancel = (event) => {
173
159
  const session = this.#session;
174
160
  if (!session) return;
@@ -179,16 +165,6 @@ var SortableController = class extends Controller {
179
165
  this.#restore(session, items);
180
166
  this.#announce("canceled", session.item);
181
167
  };
182
- /**
183
- * The slot the item was picked up from, read back through the neighbours it
184
- * had then — `null` when neither survives and the item itself is gone.
185
- *
186
- * The anchor is the item that followed it, so restoring means "before that one
187
- * again". When the anchor was deleted mid-drag the predecessor answers the
188
- * same question from the other side. With both gone the item's current slot is
189
- * the honest answer: nothing is known to have moved, so no reorder is reported
190
- * and a cancel leaves the item where it is.
191
- */
192
168
  #pickupSlot(session, items) {
193
169
  const others = items.filter((candidate) => candidate !== session.item);
194
170
  if (session.anchor) {
@@ -202,7 +178,6 @@ var SortableController = class extends Controller {
202
178
  const here = items.indexOf(session.item);
203
179
  return here === -1 ? null : here;
204
180
  }
205
- /** Puts the item back where it was picked up from. */
206
181
  #restore(session, items) {
207
182
  const slot = this.#pickupSlot(session, items);
208
183
  if (slot === null) return;
@@ -212,17 +187,6 @@ var SortableController = class extends Controller {
212
187
  slot
213
188
  );
214
189
  }
215
- /**
216
- * Keyboard stepping: `pointer-drag` reports *cumulative* synthetic deltas, so
217
- * the difference from the last consumed value is one arrow press — its sign is
218
- * the direction. Cross-axis arrows never change the primary delta (no move).
219
- *
220
- * The sign is physical (`ArrowRight` is always `+dx`), so a right-to-left row
221
- * has to invert it: there, moving the item rightward means moving it *earlier*
222
- * in the DOM. Skipping this would also split the two halves of one keypress —
223
- * `roving` already moves focus logically, so the same arrow would send the
224
- * focus and the grabbed item opposite ways.
225
- */
226
190
  #stepFromKeyboard(session, detail, items) {
227
191
  const primary = Number(this.#isVertical ? detail.dy : detail.dx) || 0;
228
192
  const delta = primary - session.lastPrimary;
@@ -239,18 +203,6 @@ var SortableController = class extends Controller {
239
203
  );
240
204
  this.#announce("moved", session.item);
241
205
  }
242
- /**
243
- * Pointer following: the item moves to the slot whose siblings' midpoints the
244
- * pointer has passed (per `orientation`).
245
- *
246
- * Only siblings that occupy space take part. A row with no layout box — a
247
- * filtered-out item, a `display: none` ancestor, a collapsed `<details>` — is
248
- * reported with an empty rect at the document origin, and its midpoint of `0`
249
- * sits below every pointer position: counted, it would read as passed on the
250
- * very first move and send the item across a slot the pointer never crossed.
251
- * With no laid-out sibling at all there is nothing to compare against and the
252
- * move is skipped entirely.
253
- */
254
206
  #followPointer(session, detail, items) {
255
207
  const pointer = Number(this.#isVertical ? detail.y : detail.x) || 0;
256
208
  const vertical = this.#isVertical;
@@ -272,18 +224,6 @@ var SortableController = class extends Controller {
272
224
  this.#insertAt(session.item, laidOut, target);
273
225
  this.#announce("moved", session.item);
274
226
  }
275
- /**
276
- * Reinserts `item` at `index` among `scope`, relative to the sibling already
277
- * standing there.
278
- *
279
- * The reorder is defined against the items, not against a container: the
280
- * neighbour's own parent is where the item belongs. A `list` target that is
281
- * not the items' parent — or none at all, which the markup contract allows —
282
- * therefore still lands the move in the right place instead of throwing, and
283
- * the last slot is *after the last item* rather than after whatever else the
284
- * container holds (a live region, a footer), which would put the row outside
285
- * the reading order the list publishes.
286
- */
287
227
  #insertAt(item, scope, index) {
288
228
  const clamped = Math.max(0, Math.min(index, scope.length));
289
229
  const ahead = scope[clamped] ?? null;
@@ -297,20 +237,6 @@ var SortableController = class extends Controller {
297
237
  parent.insertBefore(item, ahead ?? neighbour.nextSibling);
298
238
  if (hadFocus) active.focus();
299
239
  }
300
- /**
301
- * Hands one step to the page's shared announcer.
302
- *
303
- * The library carries no live region and no English copy: the wording is the
304
- * consumer's, written into `announceGrabbedText` / `announceMovedText` /
305
- * `announceDroppedText` / `announceCanceledText` with `{name}` / `{position}` /
306
- * `{total}` placeholders, and an unset one announces nothing.
307
- *
308
- * Only transitions reach here — the pickup, a step that actually changed the
309
- * landing slot, and the single end of the session — so a pointer crossing the
310
- * same slot twice or an arrow clamped at an end stays silent.
311
- *
312
- * @stimeoRuntimeOnly The texts word the steps of one reorder, spoken as they happen.
313
- */
314
240
  #announce(key, item) {
315
241
  const template = this.#announceTemplate(key);
316
242
  if (template.length === 0) return;
@@ -323,7 +249,6 @@ var SortableController = class extends Controller {
323
249
  })
324
250
  );
325
251
  }
326
- /** The consumer's wording for one step, or `""` when they authored none. */
327
252
  #announceTemplate(key) {
328
253
  switch (key) {
329
254
  case "grabbed":
@@ -336,52 +261,28 @@ var SortableController = class extends Controller {
336
261
  return this.announceCanceledTextValue;
337
262
  }
338
263
  }
339
- /** The announced item name: the authored override, else its collapsed text. */
340
264
  #nameOf(item) {
341
265
  const authored = item.getAttribute(`data-${this.identifier}-name`);
342
266
  if (authored) return authored;
343
267
  return (item.textContent ?? "").replace(/\s+/g, " ").trim();
344
268
  }
345
- /**
346
- * Resolves the sortable item owning a bubbled `pointer-drag` event.
347
- *
348
- * `pointer-drag` dispatches on its own element, and the markup contract puts
349
- * one on each item, so the owner is the item that **is** the target. Matching
350
- * an ancestor instead would make a card's own inner draggable — a knob, a
351
- * split pane, anything the primitive is composed into — drive the card.
352
- */
353
269
  #itemFor(target, items) {
354
270
  if (!target) return null;
355
271
  return items.find((item) => item === target) ?? null;
356
272
  }
357
- /** The items in live DOM order (targets re-query the DOM on every access). */
358
273
  #items() {
359
274
  return this.itemTargets;
360
275
  }
361
- /** The reorder container: the `list` target when present, else the element. */
362
276
  get #list() {
363
277
  return this.hasListTarget ? this.listTarget : this.element;
364
278
  }
365
279
  get #isVertical() {
366
280
  return this.orientationValue !== "horizontal";
367
281
  }
368
- /**
369
- * Whether DOM order runs opposite to the primary coordinate — true only for a
370
- * horizontal row under `dir="rtl"`, where the first item sits at the largest
371
- * `x`. Both drag paths reach this controller in physical terms (`pointer-drag`
372
- * documents its deltas as physical and hands RTL to its consumer, which is
373
- * this controller), so both have to be mapped back onto DOM order here.
374
- *
375
- * Read from the list, the element that lays the items out — never from the
376
- * dragged item, which may carry its own `dir`. The list inherits the computed
377
- * direction, so authoring `dir` on the root works too. A vertical list is
378
- * unaffected: writing direction does not mirror the block axis.
379
- */
380
282
  get #isReversed() {
381
283
  return !this.#isVertical && isRtl(this.#list);
382
284
  }
383
285
  };
384
286
 
385
287
  export { SortableController };
386
- //# sourceMappingURL=sortable_controller.js.map
387
288
  //# sourceMappingURL=sortable_controller.js.map