stimeo-ui 0.16.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- data/lib/stimeo/ui/version.rb +1 -1
- 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,115 @@ var FrameCoalescer = class {
|
|
|
52
35
|
}
|
|
53
36
|
};
|
|
54
37
|
|
|
55
|
-
|
|
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
|
+
schedule() {
|
|
66
|
+
if (!this.#active || this.#queued) return;
|
|
67
|
+
this.#queued = true;
|
|
68
|
+
const generation = this.#generation;
|
|
69
|
+
queueMicrotask(() => {
|
|
70
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
71
|
+
this.#queued = false;
|
|
72
|
+
this.#run();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
var MorphRenderWatcher = class {
|
|
78
|
+
#listeners = new ListenerSet();
|
|
79
|
+
#pass;
|
|
80
|
+
#element = null;
|
|
81
|
+
#onMorph = () => this.#pass.schedule();
|
|
82
|
+
constructor(run) {
|
|
83
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
84
|
+
}
|
|
85
|
+
observe(element) {
|
|
86
|
+
if (this.#element === element) return;
|
|
87
|
+
this.disconnect();
|
|
88
|
+
this.#element = element;
|
|
89
|
+
this.#pass.activate();
|
|
90
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
91
|
+
}
|
|
92
|
+
schedule() {
|
|
93
|
+
this.#pass.schedule();
|
|
94
|
+
}
|
|
95
|
+
disconnect() {
|
|
96
|
+
this.#listeners.dispose();
|
|
97
|
+
this.#pass.cancel();
|
|
98
|
+
this.#element = null;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
var NUMBER_BOUNDS = {
|
|
103
|
+
finite: { finite: true },
|
|
104
|
+
nonNegative: { finite: true, min: 0 }};
|
|
105
|
+
function matchesNumberBounds(value, bounds) {
|
|
106
|
+
if (!Number.isFinite(value)) {
|
|
107
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
108
|
+
if (direction === null) return false;
|
|
109
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
110
|
+
}
|
|
111
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
112
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
113
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
114
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
115
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readNumber(raw, fallback, bounds) {
|
|
120
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
var NumberValueReader = class {
|
|
124
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
125
|
+
read(owner, name, raw, fallback, bounds) {
|
|
126
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
127
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
128
|
+
this.#lastRejected.delete(name);
|
|
129
|
+
return resolved;
|
|
130
|
+
}
|
|
131
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
132
|
+
const literal = owner.element.getAttribute(attribute);
|
|
133
|
+
if (literal === null) {
|
|
134
|
+
this.#lastRejected.delete(name);
|
|
135
|
+
return resolved;
|
|
136
|
+
}
|
|
137
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
138
|
+
this.#lastRejected.set(name, literal);
|
|
139
|
+
console.warn(
|
|
140
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return resolved;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
56
147
|
function resolveScrollContainer(selector) {
|
|
57
148
|
const match = selector ? document.querySelector(selector) : null;
|
|
58
149
|
return match instanceof HTMLElement ? match : null;
|
|
@@ -64,64 +155,64 @@ function scrollOffset(source) {
|
|
|
64
155
|
return source === window ? window.scrollY ?? window.pageYOffset ?? 0 : source.scrollTop;
|
|
65
156
|
}
|
|
66
157
|
|
|
67
|
-
// src/controllers/smart_sticky_header_controller.ts
|
|
68
158
|
var DEFAULT_OFFSET = 80;
|
|
69
|
-
var SmartStickyHeaderController = class extends Controller {
|
|
159
|
+
var SmartStickyHeaderController = class _SmartStickyHeaderController extends Controller {
|
|
160
|
+
#numbers = new NumberValueReader();
|
|
70
161
|
static values = {
|
|
71
162
|
containerSelector: { type: String, default: "" },
|
|
72
163
|
offset: { type: Number, default: DEFAULT_OFFSET },
|
|
73
164
|
tolerance: { type: Number, default: 4 }
|
|
74
165
|
};
|
|
166
|
+
static valueConstraints = {
|
|
167
|
+
offset: NUMBER_BOUNDS.finite,
|
|
168
|
+
tolerance: NUMBER_BOUNDS.nonNegative
|
|
169
|
+
};
|
|
75
170
|
static events = ["change"];
|
|
76
|
-
#connected = false;
|
|
77
|
-
/** Coalesces scroll bursts into one measurement per frame. */
|
|
78
171
|
#frames = new FrameCoalescer();
|
|
79
|
-
/** The scroll source resolved at connect — disconnect must unbind the SAME node. */
|
|
80
172
|
#scrollerEl = window;
|
|
81
|
-
/** The validated `containerSelector`, or `""` when the declaration cannot be parsed. */
|
|
82
173
|
#containerSelector = "";
|
|
83
174
|
#lastY = 0;
|
|
84
|
-
/** Last published state, so `change` fires only on transitions. */
|
|
85
175
|
#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
176
|
get #offset() {
|
|
93
|
-
return
|
|
177
|
+
return this.#safeOffset;
|
|
94
178
|
}
|
|
95
179
|
#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
180
|
#onFocusin = () => this.#apply(false);
|
|
101
|
-
/** Validates `containerSelector` once so connect never parses a selector that throws. */
|
|
102
181
|
containerSelectorValueChanged() {
|
|
103
182
|
this.#containerSelector = validSelector(this.element, this.containerSelectorValue, "");
|
|
183
|
+
this.#morphRender.schedule();
|
|
104
184
|
}
|
|
105
|
-
/** Re-decides when application code (or a Turbo morph) changes `offset` at runtime. */
|
|
106
185
|
offsetValueChanged() {
|
|
107
|
-
|
|
186
|
+
this.#morphRender.schedule();
|
|
108
187
|
}
|
|
188
|
+
#morphRender = new MorphRenderWatcher(() => {
|
|
189
|
+
this.#followSource();
|
|
190
|
+
this.#measure();
|
|
191
|
+
});
|
|
109
192
|
connect() {
|
|
193
|
+
this.#morphRender.observe(this.element);
|
|
110
194
|
this.#hidden = null;
|
|
111
|
-
this.#
|
|
112
|
-
this.#lastY = scrollOffset(this.#scrollerEl);
|
|
113
|
-
this.#scrollerEl.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
195
|
+
this.#listenTo(resolveScrollSource(this.#containerSelector));
|
|
114
196
|
this.element.addEventListener("focusin", this.#onFocusin);
|
|
115
197
|
this.#apply(false, false);
|
|
116
|
-
this.#connected = true;
|
|
117
198
|
}
|
|
118
199
|
disconnect() {
|
|
119
|
-
this.#
|
|
200
|
+
this.#morphRender.disconnect();
|
|
120
201
|
this.#scrollerEl.removeEventListener("scroll", this.#onScroll);
|
|
121
202
|
this.element.removeEventListener("focusin", this.#onFocusin);
|
|
122
203
|
this.#frames.cancel();
|
|
123
204
|
}
|
|
124
|
-
|
|
205
|
+
#followSource() {
|
|
206
|
+
const source = resolveScrollSource(this.#containerSelector);
|
|
207
|
+
if (source === this.#scrollerEl) return;
|
|
208
|
+
this.#scrollerEl.removeEventListener("scroll", this.#onScroll);
|
|
209
|
+
this.#listenTo(source);
|
|
210
|
+
}
|
|
211
|
+
#listenTo(source) {
|
|
212
|
+
this.#scrollerEl = source;
|
|
213
|
+
this.#lastY = scrollOffset(source);
|
|
214
|
+
source.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
215
|
+
}
|
|
125
216
|
#measure() {
|
|
126
217
|
const y = scrollOffset(this.#scrollerEl);
|
|
127
218
|
if (y <= this.#offset) {
|
|
@@ -130,34 +221,42 @@ var SmartStickyHeaderController = class extends Controller {
|
|
|
130
221
|
return;
|
|
131
222
|
}
|
|
132
223
|
const delta = y - this.#lastY;
|
|
133
|
-
if (this.#isJitter(delta))
|
|
224
|
+
if (this.#isJitter(delta)) {
|
|
225
|
+
this.#apply(this.#hidden ?? false);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
134
228
|
this.#lastY = y;
|
|
135
229
|
this.#apply(delta > 0);
|
|
136
230
|
}
|
|
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
231
|
#isJitter(delta) {
|
|
144
|
-
return Math.abs(delta) < this
|
|
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
|
-
*/
|
|
232
|
+
return Math.abs(delta) < this.#safeTolerance;
|
|
233
|
+
}
|
|
152
234
|
#apply(hidden, notify = true) {
|
|
153
235
|
if (hidden && this.element.contains(document.activeElement)) return;
|
|
154
|
-
|
|
236
|
+
const changed = hidden !== this.#hidden;
|
|
155
237
|
this.#hidden = hidden;
|
|
156
238
|
this.element.setAttribute("data-header-hidden", hidden ? "true" : "false");
|
|
157
|
-
if (notify) this.dispatch("change", { detail: { hidden } });
|
|
239
|
+
if (notify && changed) this.dispatch("change", { detail: { hidden } });
|
|
240
|
+
}
|
|
241
|
+
get #safeOffset() {
|
|
242
|
+
return this.#numbers.read(
|
|
243
|
+
this,
|
|
244
|
+
"offset",
|
|
245
|
+
this.offsetValue,
|
|
246
|
+
_SmartStickyHeaderController.values.offset.default,
|
|
247
|
+
_SmartStickyHeaderController.valueConstraints.offset
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
get #safeTolerance() {
|
|
251
|
+
return this.#numbers.read(
|
|
252
|
+
this,
|
|
253
|
+
"tolerance",
|
|
254
|
+
this.toleranceValue,
|
|
255
|
+
_SmartStickyHeaderController.values.tolerance.default,
|
|
256
|
+
_SmartStickyHeaderController.valueConstraints.tolerance
|
|
257
|
+
);
|
|
158
258
|
}
|
|
159
259
|
};
|
|
160
260
|
|
|
161
261
|
export { SmartStickyHeaderController };
|
|
162
|
-
//# sourceMappingURL=smart_sticky_header_controller.js.map
|
|
163
262
|
//# sourceMappingURL=smart_sticky_header_controller.js.map
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/sortable_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/announce.ts
|
|
6
3
|
function announce(message, options = {}) {
|
|
7
4
|
const text = message.trim();
|
|
8
5
|
if (text.length === 0) return;
|
|
@@ -19,32 +16,26 @@ function fillTemplate(template, values) {
|
|
|
19
16
|
});
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
// src/utils/logical_scroll.ts
|
|
23
19
|
function isRtl(element) {
|
|
24
20
|
return window.getComputedStyle(element).direction === "rtl";
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
// src/utils/microtask_coalescer.ts
|
|
28
23
|
var MicrotaskCoalescer = class {
|
|
29
24
|
#run;
|
|
30
25
|
#queued = false;
|
|
31
26
|
#active = false;
|
|
32
27
|
#generation = 0;
|
|
33
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
34
28
|
constructor(run) {
|
|
35
29
|
this.#run = run;
|
|
36
30
|
}
|
|
37
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
38
31
|
activate() {
|
|
39
32
|
this.#active = true;
|
|
40
33
|
}
|
|
41
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
42
34
|
cancel() {
|
|
43
35
|
this.#active = false;
|
|
44
36
|
this.#queued = false;
|
|
45
37
|
this.#generation += 1;
|
|
46
38
|
}
|
|
47
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
48
39
|
schedule() {
|
|
49
40
|
if (!this.#active || this.#queued) return;
|
|
50
41
|
this.#queued = true;
|
|
@@ -57,19 +48,16 @@ var MicrotaskCoalescer = class {
|
|
|
57
48
|
}
|
|
58
49
|
};
|
|
59
50
|
|
|
60
|
-
// src/utils/transient_hooks.ts
|
|
61
51
|
var TransientHooks = class {
|
|
62
52
|
#attributes;
|
|
63
53
|
constructor(options) {
|
|
64
54
|
this.#attributes = options.attributes;
|
|
65
55
|
}
|
|
66
|
-
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
67
56
|
reset(element) {
|
|
68
57
|
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
69
58
|
}
|
|
70
59
|
};
|
|
71
60
|
|
|
72
|
-
// src/controllers/sortable_controller.ts
|
|
73
61
|
var TRANSIENT = new TransientHooks({ attributes: ["data-sortable-dragging"] });
|
|
74
62
|
var SortableController = class extends Controller {
|
|
75
63
|
static targets = ["list", "item"];
|
|
@@ -82,7 +70,6 @@ var SortableController = class extends Controller {
|
|
|
82
70
|
};
|
|
83
71
|
static events = ["reorder"];
|
|
84
72
|
#session = null;
|
|
85
|
-
/** Defers the lost-item check to after the mutation batch (see below). */
|
|
86
73
|
#settle = new MicrotaskCoalescer(() => this.#dropLostSession());
|
|
87
74
|
connect() {
|
|
88
75
|
TRANSIENT.reset(this.element);
|
|
@@ -101,17 +88,6 @@ var SortableController = class extends Controller {
|
|
|
101
88
|
this.#session = null;
|
|
102
89
|
this.element.removeAttribute("data-sortable-dragging");
|
|
103
90
|
}
|
|
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
91
|
itemTargetDisconnected(item) {
|
|
116
92
|
if (this.#session?.item !== item) return;
|
|
117
93
|
this.#settle.schedule();
|
|
@@ -125,7 +101,6 @@ var SortableController = class extends Controller {
|
|
|
125
101
|
this.element.removeAttribute("data-sortable-dragging");
|
|
126
102
|
if (session.item.isConnected) this.#restore(session, items);
|
|
127
103
|
}
|
|
128
|
-
/** Picks the item up: remembers its neighbours and announces the grab. */
|
|
129
104
|
#onDragStart = (event) => {
|
|
130
105
|
if (this.#session) return;
|
|
131
106
|
const items = this.#items();
|
|
@@ -153,7 +128,6 @@ var SortableController = class extends Controller {
|
|
|
153
128
|
this.#followPointer(session, detail, items);
|
|
154
129
|
}
|
|
155
130
|
};
|
|
156
|
-
/** Drops the item: announces, then reports `reorder` if the position changed. */
|
|
157
131
|
#onDragEnd = (event) => {
|
|
158
132
|
const session = this.#session;
|
|
159
133
|
if (!session) return;
|
|
@@ -168,7 +142,6 @@ var SortableController = class extends Controller {
|
|
|
168
142
|
this.dispatch("reorder", { detail: { item: session.item, from, to } });
|
|
169
143
|
}
|
|
170
144
|
};
|
|
171
|
-
/** Restores the pickup position (Escape / OS `pointercancel`). */
|
|
172
145
|
#onDragCancel = (event) => {
|
|
173
146
|
const session = this.#session;
|
|
174
147
|
if (!session) return;
|
|
@@ -179,16 +152,6 @@ var SortableController = class extends Controller {
|
|
|
179
152
|
this.#restore(session, items);
|
|
180
153
|
this.#announce("canceled", session.item);
|
|
181
154
|
};
|
|
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
155
|
#pickupSlot(session, items) {
|
|
193
156
|
const others = items.filter((candidate) => candidate !== session.item);
|
|
194
157
|
if (session.anchor) {
|
|
@@ -202,7 +165,6 @@ var SortableController = class extends Controller {
|
|
|
202
165
|
const here = items.indexOf(session.item);
|
|
203
166
|
return here === -1 ? null : here;
|
|
204
167
|
}
|
|
205
|
-
/** Puts the item back where it was picked up from. */
|
|
206
168
|
#restore(session, items) {
|
|
207
169
|
const slot = this.#pickupSlot(session, items);
|
|
208
170
|
if (slot === null) return;
|
|
@@ -212,17 +174,6 @@ var SortableController = class extends Controller {
|
|
|
212
174
|
slot
|
|
213
175
|
);
|
|
214
176
|
}
|
|
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
177
|
#stepFromKeyboard(session, detail, items) {
|
|
227
178
|
const primary = Number(this.#isVertical ? detail.dy : detail.dx) || 0;
|
|
228
179
|
const delta = primary - session.lastPrimary;
|
|
@@ -239,18 +190,6 @@ var SortableController = class extends Controller {
|
|
|
239
190
|
);
|
|
240
191
|
this.#announce("moved", session.item);
|
|
241
192
|
}
|
|
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
193
|
#followPointer(session, detail, items) {
|
|
255
194
|
const pointer = Number(this.#isVertical ? detail.y : detail.x) || 0;
|
|
256
195
|
const vertical = this.#isVertical;
|
|
@@ -272,18 +211,6 @@ var SortableController = class extends Controller {
|
|
|
272
211
|
this.#insertAt(session.item, laidOut, target);
|
|
273
212
|
this.#announce("moved", session.item);
|
|
274
213
|
}
|
|
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
214
|
#insertAt(item, scope, index) {
|
|
288
215
|
const clamped = Math.max(0, Math.min(index, scope.length));
|
|
289
216
|
const ahead = scope[clamped] ?? null;
|
|
@@ -297,20 +224,6 @@ var SortableController = class extends Controller {
|
|
|
297
224
|
parent.insertBefore(item, ahead ?? neighbour.nextSibling);
|
|
298
225
|
if (hadFocus) active.focus();
|
|
299
226
|
}
|
|
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
227
|
#announce(key, item) {
|
|
315
228
|
const template = this.#announceTemplate(key);
|
|
316
229
|
if (template.length === 0) return;
|
|
@@ -323,7 +236,6 @@ var SortableController = class extends Controller {
|
|
|
323
236
|
})
|
|
324
237
|
);
|
|
325
238
|
}
|
|
326
|
-
/** The consumer's wording for one step, or `""` when they authored none. */
|
|
327
239
|
#announceTemplate(key) {
|
|
328
240
|
switch (key) {
|
|
329
241
|
case "grabbed":
|
|
@@ -336,52 +248,28 @@ var SortableController = class extends Controller {
|
|
|
336
248
|
return this.announceCanceledTextValue;
|
|
337
249
|
}
|
|
338
250
|
}
|
|
339
|
-
/** The announced item name: the authored override, else its collapsed text. */
|
|
340
251
|
#nameOf(item) {
|
|
341
252
|
const authored = item.getAttribute(`data-${this.identifier}-name`);
|
|
342
253
|
if (authored) return authored;
|
|
343
254
|
return (item.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
344
255
|
}
|
|
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
256
|
#itemFor(target, items) {
|
|
354
257
|
if (!target) return null;
|
|
355
258
|
return items.find((item) => item === target) ?? null;
|
|
356
259
|
}
|
|
357
|
-
/** The items in live DOM order (targets re-query the DOM on every access). */
|
|
358
260
|
#items() {
|
|
359
261
|
return this.itemTargets;
|
|
360
262
|
}
|
|
361
|
-
/** The reorder container: the `list` target when present, else the element. */
|
|
362
263
|
get #list() {
|
|
363
264
|
return this.hasListTarget ? this.listTarget : this.element;
|
|
364
265
|
}
|
|
365
266
|
get #isVertical() {
|
|
366
267
|
return this.orientationValue !== "horizontal";
|
|
367
268
|
}
|
|
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
269
|
get #isReversed() {
|
|
381
270
|
return !this.#isVertical && isRtl(this.#list);
|
|
382
271
|
}
|
|
383
272
|
};
|
|
384
273
|
|
|
385
274
|
export { SortableController };
|
|
386
|
-
//# sourceMappingURL=sortable_controller.js.map
|
|
387
275
|
//# sourceMappingURL=sortable_controller.js.map
|