stimeo-ui 0.3.0 → 0.5.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 +124 -0
- data/dist/controllers/alert_dialog_controller.js +32 -5
- data/dist/controllers/announcer_controller.js +255 -20
- data/dist/controllers/aspect_ratio_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +5 -1
- data/dist/controllers/carousel_controller.js +5 -1
- data/dist/controllers/clipboard_controller.js +8 -3
- data/dist/controllers/collapsible_controller.js +4 -1
- data/dist/controllers/color_picker_controller.js +52 -2
- data/dist/controllers/command_palette_controller.js +32 -5
- data/dist/controllers/confirm_controller.js +32 -5
- data/dist/controllers/context_menu_controller.js +2 -2
- data/dist/controllers/countdown_controller.js +117 -13
- data/dist/controllers/date_range_picker_controller.js +55 -1
- data/dist/controllers/dialog_controller.js +32 -5
- data/dist/controllers/direct_upload_controller.js +3 -3
- data/dist/controllers/drawer_controller.js +32 -5
- data/dist/controllers/empty_state_controller.js +128 -23
- data/dist/controllers/flash_controller.js +161 -21
- data/dist/controllers/focus_controller.js +32 -5
- data/dist/controllers/form_validation_controller.js +8 -2
- data/dist/controllers/frame_loading_controller.js +261 -27
- data/dist/controllers/highlight_controller.js +38 -1
- data/dist/controllers/idle_controller.js +13 -2
- data/dist/controllers/local_time_controller.js +102 -6
- data/dist/controllers/masonry_controller.js +1 -1
- data/dist/controllers/meter_controller.js +147 -26
- data/dist/controllers/network_status_controller.js +29 -11
- data/dist/controllers/number_input_controller.js +191 -24
- data/dist/controllers/overflow_menu_controller.js +34 -7
- data/dist/controllers/pagination_controller.js +5 -1
- data/dist/controllers/password_strength_controller.js +1 -1
- data/dist/controllers/pointer_drag_controller.js +10 -0
- data/dist/controllers/portal_controller.js +10 -0
- data/dist/controllers/progress_controller.js +123 -12
- data/dist/controllers/range_slider_controller.js +449 -93
- data/dist/controllers/rating_controller.js +55 -0
- data/dist/controllers/relative_time_controller.js +135 -12
- data/dist/controllers/scroll_area_controller.js +1 -1
- data/dist/controllers/separator_controller.js +13 -17
- data/dist/controllers/sidebar_controller.js +37 -8
- data/dist/controllers/skeleton_controller.js +143 -22
- data/dist/controllers/slider_controller.js +342 -50
- data/dist/controllers/spinner_controller.js +244 -28
- data/dist/controllers/step_indicator_controller.js +85 -6
- data/dist/controllers/stepper_controller.js +2 -0
- data/dist/controllers/stick_to_bottom_controller.js +60 -10
- data/dist/controllers/switch_controller.js +162 -18
- data/dist/controllers/textarea_autosize_controller.js +1 -1
- data/dist/controllers/time_picker_controller.js +6 -3
- data/dist/controllers/tree_view_controller.js +19 -1
- data/dist/index.js +2278 -596
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -18,14 +18,224 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
18
18
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// src/utils/microtask_coalescer.ts
|
|
22
|
+
var MicrotaskCoalescer = class {
|
|
23
|
+
#run;
|
|
24
|
+
#queued = false;
|
|
25
|
+
#active = false;
|
|
26
|
+
#generation = 0;
|
|
27
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
28
|
+
constructor(run) {
|
|
29
|
+
this.#run = run;
|
|
30
|
+
}
|
|
31
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
32
|
+
activate() {
|
|
33
|
+
this.#active = true;
|
|
34
|
+
}
|
|
35
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
36
|
+
cancel() {
|
|
37
|
+
this.#active = false;
|
|
38
|
+
this.#queued = false;
|
|
39
|
+
this.#generation += 1;
|
|
40
|
+
}
|
|
41
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
42
|
+
schedule() {
|
|
43
|
+
if (!this.#active || this.#queued) return;
|
|
44
|
+
this.#queued = true;
|
|
45
|
+
const generation = this.#generation;
|
|
46
|
+
queueMicrotask(() => {
|
|
47
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
48
|
+
this.#queued = false;
|
|
49
|
+
this.#run();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/utils/owned_pointer_session.ts
|
|
55
|
+
var OwnedPointerSession = class {
|
|
56
|
+
pointerId;
|
|
57
|
+
#owner;
|
|
58
|
+
#handlers;
|
|
59
|
+
#abort = new AbortController();
|
|
60
|
+
#active = true;
|
|
61
|
+
constructor(start, owner, handlers) {
|
|
62
|
+
this.pointerId = start.pointerId;
|
|
63
|
+
this.#owner = owner;
|
|
64
|
+
this.#handlers = handlers;
|
|
65
|
+
const { signal } = this.#abort;
|
|
66
|
+
owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
|
|
67
|
+
owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
|
|
68
|
+
owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
|
|
69
|
+
owner.addEventListener("lostpointercapture", this.#onLostCapture, { signal });
|
|
70
|
+
try {
|
|
71
|
+
owner.setPointerCapture?.(this.pointerId);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Whether this session still owns its pointer and listeners. */
|
|
76
|
+
get active() {
|
|
77
|
+
return this.#active;
|
|
78
|
+
}
|
|
79
|
+
/** Whether `event` belongs to the initiating pointer of the live session. */
|
|
80
|
+
owns(event) {
|
|
81
|
+
return this.#active && event.pointerId === this.pointerId;
|
|
82
|
+
}
|
|
83
|
+
/** Releases capture/listeners and invokes the end callback exactly once. */
|
|
84
|
+
end() {
|
|
85
|
+
if (!this.#active) return;
|
|
86
|
+
this.#active = false;
|
|
87
|
+
this.#abort.abort();
|
|
88
|
+
try {
|
|
89
|
+
this.#owner.releasePointerCapture?.(this.pointerId);
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
this.#handlers.end?.();
|
|
93
|
+
}
|
|
94
|
+
#onMove = (event) => {
|
|
95
|
+
if (this.owns(event)) this.#handlers.move(event);
|
|
96
|
+
};
|
|
97
|
+
#onEndEvent = (event) => {
|
|
98
|
+
if (this.owns(event)) this.end();
|
|
99
|
+
};
|
|
100
|
+
#onLostCapture = (event) => {
|
|
101
|
+
const pointerId = event.pointerId;
|
|
102
|
+
if (typeof pointerId === "number" && pointerId !== this.pointerId) return;
|
|
103
|
+
this.end();
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/utils/range.ts
|
|
108
|
+
function rangeFraction(value, min, max) {
|
|
109
|
+
const span = max - min;
|
|
110
|
+
if (!(span > 0)) return 0;
|
|
111
|
+
const clamped = Math.min(max, Math.max(min, value));
|
|
112
|
+
let fraction;
|
|
113
|
+
if (Number.isFinite(span)) {
|
|
114
|
+
fraction = (clamped - min) / span;
|
|
115
|
+
} else {
|
|
116
|
+
const scale = Math.max(Math.abs(min), Math.abs(max));
|
|
117
|
+
fraction = (clamped / scale - min / scale) / (max / scale - min / scale);
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isFinite(fraction)) return 0;
|
|
120
|
+
return Math.min(1, Math.max(0, fraction));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/utils/stepped_value.ts
|
|
124
|
+
function effectiveStep(step) {
|
|
125
|
+
return Number.isFinite(step) && step > 0 ? step : 1;
|
|
126
|
+
}
|
|
127
|
+
function snapSteppedValue(raw, range) {
|
|
128
|
+
if (!(range.min <= range.max)) return finiteFallback(range.min, range.max);
|
|
129
|
+
const input = Number.isNaN(raw) ? finiteFallback(range.min, range.max) : raw;
|
|
130
|
+
const clamped = Math.min(range.max, Math.max(range.min, input));
|
|
131
|
+
const step = effectiveStep(range.step);
|
|
132
|
+
const base = stepBase(range);
|
|
133
|
+
const candidates = [];
|
|
134
|
+
if (Number.isFinite(range.min)) candidates.push(range.min);
|
|
135
|
+
if (Number.isFinite(range.max)) candidates.push(range.max);
|
|
136
|
+
const gridPosition = (clamped - base) / step;
|
|
137
|
+
if (Number.isFinite(gridPosition)) {
|
|
138
|
+
addGridCandidate(candidates, Math.floor(gridPosition), range, base, step);
|
|
139
|
+
addGridCandidate(candidates, Math.ceil(gridPosition), range, base, step);
|
|
140
|
+
}
|
|
141
|
+
if (candidates.length === 0) return clamped;
|
|
142
|
+
let nearest = candidates[0];
|
|
143
|
+
let nearestDistance = Math.abs(clamped - nearest);
|
|
144
|
+
for (const candidate of candidates.slice(1)) {
|
|
145
|
+
const distance = Math.abs(clamped - candidate);
|
|
146
|
+
if (distance < nearestDistance || nearlyEqual(distance, nearestDistance) && candidate > nearest) {
|
|
147
|
+
nearest = candidate;
|
|
148
|
+
nearestDistance = distance;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return nearest;
|
|
152
|
+
}
|
|
153
|
+
function stepSteppedValue(current, count, range) {
|
|
154
|
+
const value = snapSteppedValue(current, range);
|
|
155
|
+
const distance = Math.abs(Math.trunc(count));
|
|
156
|
+
if (!Number.isFinite(distance) || distance === 0) return value;
|
|
157
|
+
const direction = Math.sign(count);
|
|
158
|
+
const adjacent = adjacentSteppedValue(value, direction, range);
|
|
159
|
+
const raw = adjacent + direction * (distance - 1) * effectiveStep(range.step);
|
|
160
|
+
return snapSteppedValue(raw, range);
|
|
161
|
+
}
|
|
162
|
+
function adjacentSteppedValue(current, direction, range) {
|
|
163
|
+
const step = effectiveStep(range.step);
|
|
164
|
+
const base = stepBase(range);
|
|
165
|
+
const candidates = [];
|
|
166
|
+
if (direction > 0) {
|
|
167
|
+
const position2 = (current - base) / step;
|
|
168
|
+
if (Number.isFinite(position2)) {
|
|
169
|
+
let gridIndex = Math.floor(position2) + 1;
|
|
170
|
+
let candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
171
|
+
if (candidate < current || nearlyEqual(candidate, current)) {
|
|
172
|
+
gridIndex += 1;
|
|
173
|
+
candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
174
|
+
}
|
|
175
|
+
if (candidate > current && !nearlyEqual(candidate, current) && within(candidate, range.min, range.max)) {
|
|
176
|
+
candidates.push(clamp(candidate, range));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return clamp(Math.min(...candidates), range);
|
|
180
|
+
}
|
|
181
|
+
const position = (current - base) / step;
|
|
182
|
+
if (Number.isFinite(position)) {
|
|
183
|
+
let gridIndex = Math.ceil(position) - 1;
|
|
184
|
+
let candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
185
|
+
if (candidate > current || nearlyEqual(candidate, current)) {
|
|
186
|
+
gridIndex -= 1;
|
|
187
|
+
candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
188
|
+
}
|
|
189
|
+
if (candidate < current && !nearlyEqual(candidate, current) && within(candidate, range.min, range.max)) {
|
|
190
|
+
candidates.push(clamp(candidate, range));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return clamp(Math.max(...candidates), range);
|
|
194
|
+
}
|
|
195
|
+
function addGridCandidate(candidates, index, range, base, step) {
|
|
196
|
+
const candidate = cleanGridValue(base + index * step, base, step);
|
|
197
|
+
if (within(candidate, range.min, range.max)) candidates.push(clamp(candidate, range));
|
|
198
|
+
}
|
|
199
|
+
function stepBase(range) {
|
|
200
|
+
if (range.base !== void 0 && Number.isFinite(range.base)) return range.base;
|
|
201
|
+
return Number.isFinite(range.min) ? range.min : 0;
|
|
202
|
+
}
|
|
203
|
+
function cleanGridValue(value, base, step) {
|
|
204
|
+
const precision = Math.max(decimalPlaces(base), decimalPlaces(step));
|
|
205
|
+
return precision <= 100 ? Number(value.toFixed(precision)) : value;
|
|
206
|
+
}
|
|
207
|
+
function decimalPlaces(value) {
|
|
208
|
+
const [coefficient = "", exponentText] = Math.abs(value).toString().toLowerCase().split("e");
|
|
209
|
+
const fractionLength = coefficient.split(".")[1]?.length ?? 0;
|
|
210
|
+
const exponent = exponentText === void 0 ? 0 : Number(exponentText);
|
|
211
|
+
return Math.max(0, fractionLength - exponent);
|
|
212
|
+
}
|
|
213
|
+
function within(value, min, max) {
|
|
214
|
+
return (value > min || nearlyEqual(value, min)) && (value < max || nearlyEqual(value, max));
|
|
215
|
+
}
|
|
216
|
+
function clamp(value, range) {
|
|
217
|
+
return Math.min(range.max, Math.max(range.min, value));
|
|
218
|
+
}
|
|
219
|
+
function nearlyEqual(left, right) {
|
|
220
|
+
const scale = Math.max(Math.abs(left), Math.abs(right));
|
|
221
|
+
return Number.isFinite(left) && Number.isFinite(right) && Math.abs(left - right) <= Number.EPSILON * scale;
|
|
222
|
+
}
|
|
223
|
+
function finiteFallback(min, max) {
|
|
224
|
+
if (Number.isFinite(min)) return min;
|
|
225
|
+
if (Number.isFinite(max)) return max;
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
21
229
|
// src/controllers/range_slider_controller.ts
|
|
22
|
-
var START_PROPERTY = "--stimeo
|
|
23
|
-
var END_PROPERTY = "--stimeo
|
|
230
|
+
var START_PROPERTY = "--stimeo--range-slider-start";
|
|
231
|
+
var END_PROPERTY = "--stimeo--range-slider-end";
|
|
232
|
+
var DEFAULT_MIN = 0;
|
|
233
|
+
var DEFAULT_MAX = 100;
|
|
24
234
|
var RangeSliderController = class extends Controller {
|
|
25
235
|
static targets = ["track", "startThumb", "endThumb"];
|
|
26
236
|
static values = {
|
|
27
|
-
min: { type: Number, default:
|
|
28
|
-
max: { type: Number, default:
|
|
237
|
+
min: { type: Number, default: DEFAULT_MIN },
|
|
238
|
+
max: { type: Number, default: DEFAULT_MAX },
|
|
29
239
|
step: { type: Number, default: 1 },
|
|
30
240
|
start: { type: Number, default: 0 },
|
|
31
241
|
end: { type: Number, default: 100 },
|
|
@@ -33,159 +243,305 @@ var RangeSliderController = class extends Controller {
|
|
|
33
243
|
};
|
|
34
244
|
static actions = ["onKeydown", "onPointerDown"];
|
|
35
245
|
static events = ["change"];
|
|
36
|
-
/**
|
|
37
|
-
#
|
|
246
|
+
/** One initiating pointer owns each live drag and its stable target snapshot. */
|
|
247
|
+
#drag = null;
|
|
38
248
|
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
39
249
|
get #mirrored() {
|
|
40
250
|
return this.logicalTrackValue && isRtl(this.element);
|
|
41
251
|
}
|
|
42
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* Collapses a morph that swaps render inputs into one repaint, and refuses the
|
|
254
|
+
* pass Stimulus delivers before `connect()`.
|
|
255
|
+
*/
|
|
256
|
+
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
43
257
|
connect() {
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
this.#
|
|
258
|
+
this.#repaint.activate();
|
|
259
|
+
const range = this.#effectiveRange;
|
|
260
|
+
const pair = this.#currentPair(range);
|
|
261
|
+
this.#commit(pair.start, pair.end, null, false);
|
|
47
262
|
}
|
|
48
263
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
49
264
|
disconnect() {
|
|
50
|
-
this.#
|
|
51
|
-
this.#
|
|
265
|
+
this.#repaint.cancel();
|
|
266
|
+
this.#endDrag();
|
|
267
|
+
}
|
|
268
|
+
/** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
|
|
269
|
+
minValueChanged() {
|
|
270
|
+
this.#repaint.schedule();
|
|
271
|
+
}
|
|
272
|
+
/** Repaints when application code (or a Turbo morph) changes `max` at runtime. */
|
|
273
|
+
maxValueChanged() {
|
|
274
|
+
this.#repaint.schedule();
|
|
275
|
+
}
|
|
276
|
+
/** Repaints when application code (or a Turbo morph) changes `step` at runtime. */
|
|
277
|
+
stepValueChanged() {
|
|
278
|
+
this.#repaint.schedule();
|
|
279
|
+
}
|
|
280
|
+
/** Repaints when application code (or a Turbo morph) changes `start` at runtime. */
|
|
281
|
+
startValueChanged() {
|
|
282
|
+
this.#repaint.schedule();
|
|
283
|
+
}
|
|
284
|
+
/** Repaints when application code (or a Turbo morph) changes `end` at runtime. */
|
|
285
|
+
endValueChanged() {
|
|
286
|
+
this.#repaint.schedule();
|
|
287
|
+
}
|
|
288
|
+
/** Hydrates a replacement start thumb and restores live-drag focus ownership. */
|
|
289
|
+
startThumbTargetConnected(thumb) {
|
|
290
|
+
const range = this.#effectiveRange;
|
|
291
|
+
const pair = this.#currentPair(range);
|
|
292
|
+
this.#renderStartThumb(thumb, pair, range);
|
|
293
|
+
this.#renderFractions(pair, range);
|
|
294
|
+
if (this.#drag?.kind === "start" && this.#drag.thumb === null) {
|
|
295
|
+
this.#drag.thumb = thumb;
|
|
296
|
+
thumb.focus();
|
|
297
|
+
}
|
|
298
|
+
this.#repaint.schedule();
|
|
299
|
+
}
|
|
300
|
+
/** Drops a stale start-thumb reference without orphaning the track gesture. */
|
|
301
|
+
startThumbTargetDisconnected(thumb) {
|
|
302
|
+
if (this.#drag?.kind === "start" && this.#drag.thumb === thumb) this.#drag.thumb = null;
|
|
303
|
+
}
|
|
304
|
+
/** Hydrates a replacement end thumb and restores live-drag focus ownership. */
|
|
305
|
+
endThumbTargetConnected(thumb) {
|
|
306
|
+
const range = this.#effectiveRange;
|
|
307
|
+
const pair = this.#currentPair(range);
|
|
308
|
+
this.#renderEndThumb(thumb, pair, range);
|
|
309
|
+
this.#renderFractions(pair, range);
|
|
310
|
+
if (this.#drag?.kind === "end" && this.#drag.thumb === null) {
|
|
311
|
+
this.#drag.thumb = thumb;
|
|
312
|
+
thumb.focus();
|
|
313
|
+
}
|
|
314
|
+
this.#repaint.schedule();
|
|
315
|
+
}
|
|
316
|
+
/** Drops a stale end-thumb reference without orphaning the track gesture. */
|
|
317
|
+
endThumbTargetDisconnected(thumb) {
|
|
318
|
+
if (this.#drag?.kind === "end" && this.#drag.thumb === thumb) this.#drag.thumb = null;
|
|
319
|
+
}
|
|
320
|
+
/** Ends a gesture whose geometry target disappeared or ceased being a target. */
|
|
321
|
+
trackTargetDisconnected(track) {
|
|
322
|
+
if (this.#drag?.track === track) this.#endDrag();
|
|
52
323
|
}
|
|
53
324
|
/** Keyboard stepping for whichever thumb is focused (the action's element). */
|
|
54
325
|
onKeydown(event) {
|
|
55
326
|
if (isReservedArrowChord(event)) return;
|
|
56
327
|
const thumb = event.currentTarget;
|
|
57
328
|
const isStart = this.hasStartThumbTarget && thumb === this.startThumbTarget;
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
329
|
+
const isEnd = this.hasEndThumbTarget && thumb === this.endThumbTarget;
|
|
330
|
+
const effectiveRange = this.#effectiveRange;
|
|
331
|
+
const pair = this.#currentPair(effectiveRange);
|
|
332
|
+
let kind;
|
|
333
|
+
let current;
|
|
334
|
+
let range;
|
|
335
|
+
if (isStart) {
|
|
336
|
+
kind = "start";
|
|
337
|
+
current = pair.start;
|
|
338
|
+
range = {
|
|
339
|
+
min: effectiveRange.min,
|
|
340
|
+
max: pair.end,
|
|
341
|
+
step: effectiveRange.step,
|
|
342
|
+
base: effectiveRange.min
|
|
343
|
+
};
|
|
344
|
+
} else {
|
|
345
|
+
if (!isEnd) return;
|
|
346
|
+
kind = "end";
|
|
347
|
+
current = pair.end;
|
|
348
|
+
range = {
|
|
349
|
+
min: pair.start,
|
|
350
|
+
max: effectiveRange.max,
|
|
351
|
+
step: effectiveRange.step,
|
|
352
|
+
base: effectiveRange.min
|
|
353
|
+
};
|
|
354
|
+
}
|
|
62
355
|
let next = null;
|
|
63
356
|
switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {
|
|
64
357
|
case "ArrowRight":
|
|
65
358
|
case "ArrowUp":
|
|
66
|
-
next = current
|
|
359
|
+
next = stepSteppedValue(current, 1, range);
|
|
67
360
|
break;
|
|
68
361
|
case "ArrowLeft":
|
|
69
362
|
case "ArrowDown":
|
|
70
|
-
next = current -
|
|
363
|
+
next = stepSteppedValue(current, -1, range);
|
|
71
364
|
break;
|
|
72
365
|
case "PageUp":
|
|
73
|
-
next = current
|
|
366
|
+
next = stepSteppedValue(current, 10, range);
|
|
74
367
|
break;
|
|
75
368
|
case "PageDown":
|
|
76
|
-
next = current -
|
|
369
|
+
next = stepSteppedValue(current, -10, range);
|
|
77
370
|
break;
|
|
78
371
|
case "Home":
|
|
79
|
-
next =
|
|
372
|
+
next = range.min;
|
|
80
373
|
break;
|
|
81
374
|
case "End":
|
|
82
|
-
next =
|
|
375
|
+
next = range.max;
|
|
83
376
|
break;
|
|
84
377
|
default:
|
|
85
378
|
return;
|
|
86
379
|
}
|
|
87
380
|
event.preventDefault();
|
|
88
|
-
this.#moveThumb(
|
|
381
|
+
this.#moveThumb(kind, next);
|
|
89
382
|
}
|
|
90
383
|
/** Begins a pointer drag on the track, moving the thumb nearest the press. */
|
|
91
384
|
onPointerDown(event) {
|
|
92
|
-
if (!this.hasTrackTarget)
|
|
385
|
+
if (event.button !== 0 || this.#drag || !this.hasTrackTarget || !this.hasStartThumbTarget || !this.hasEndThumbTarget) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const track = this.trackTarget;
|
|
93
389
|
const mirrored = this.#mirrored;
|
|
94
|
-
const value = this.#valueFromClientX(event.clientX, mirrored);
|
|
390
|
+
const value = this.#valueFromClientX(event.clientX, mirrored, track);
|
|
95
391
|
if (value === null) return;
|
|
96
392
|
event.preventDefault();
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
393
|
+
const pair = this.#currentPair(this.#effectiveRange);
|
|
394
|
+
const kind = this.#nearestThumb(value, pair);
|
|
395
|
+
let thumb;
|
|
396
|
+
if (kind === "start") thumb = this.startThumbTarget;
|
|
397
|
+
else thumb = this.endThumbTarget;
|
|
398
|
+
this.#moveThumb(kind, value);
|
|
399
|
+
thumb.focus();
|
|
400
|
+
const drag = { pointer: null, track, thumb, kind };
|
|
401
|
+
drag.pointer = new OwnedPointerSession(event, track, {
|
|
402
|
+
move: (move) => {
|
|
403
|
+
if (!track.isConnected) {
|
|
404
|
+
this.#endDrag();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const moved = this.#valueFromClientX(move.clientX, mirrored, track);
|
|
408
|
+
if (moved !== null) this.#moveThumb(kind, moved);
|
|
409
|
+
},
|
|
410
|
+
end: () => {
|
|
411
|
+
if (this.#drag === drag) this.#drag = null;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
this.#drag = drag;
|
|
118
415
|
}
|
|
119
416
|
/** Maps a pointer X coordinate to a raw value using the track geometry. */
|
|
120
|
-
#valueFromClientX(clientX, mirrored) {
|
|
121
|
-
const rect =
|
|
417
|
+
#valueFromClientX(clientX, mirrored, track) {
|
|
418
|
+
const rect = track.getBoundingClientRect();
|
|
122
419
|
if (rect.width === 0) return null;
|
|
123
420
|
const offset = (clientX - rect.left) / rect.width;
|
|
124
421
|
const fraction = mirrored ? 1 - offset : offset;
|
|
125
|
-
|
|
422
|
+
const range = this.#effectiveRange;
|
|
423
|
+
return range.min + fraction * (range.max - range.min);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Chooses the closest thumb while keeping an overlapped pair expandable.
|
|
427
|
+
* Ordinary midpoint ties stay deterministic on `start`; at an overlap, a
|
|
428
|
+
* press above the shared value selects `end` and a press below selects
|
|
429
|
+
* `start`.
|
|
430
|
+
*/
|
|
431
|
+
#nearestThumb(value, pair) {
|
|
432
|
+
const startDistance = Math.abs(value - pair.start);
|
|
433
|
+
const endDistance = Math.abs(value - pair.end);
|
|
434
|
+
if (endDistance < startDistance) return "end";
|
|
435
|
+
if (pair.start === pair.end && value > pair.start) return "end";
|
|
436
|
+
return "start";
|
|
126
437
|
}
|
|
127
438
|
/** Moves one thumb to a new raw value, keeping the pair ordered. */
|
|
128
|
-
#moveThumb(
|
|
129
|
-
|
|
130
|
-
|
|
439
|
+
#moveThumb(kind, raw) {
|
|
440
|
+
const pair = this.#currentPair(this.#effectiveRange);
|
|
441
|
+
if (kind === "start") {
|
|
442
|
+
this.#commit(raw, pair.end, "start", true);
|
|
131
443
|
} else {
|
|
132
|
-
this.#commit(
|
|
444
|
+
this.#commit(pair.start, raw, "end", true);
|
|
133
445
|
}
|
|
134
446
|
}
|
|
135
447
|
/**
|
|
136
448
|
* Clamps and snaps `start`/`end`, enforces `start ≤ end`, stores the pair, and
|
|
137
449
|
* reflects it onto the thumbs' ARIA attributes and the range custom
|
|
138
|
-
* properties. Dispatches `change` only
|
|
450
|
+
* properties. Dispatches `stimeo--range-slider:change` only when a
|
|
451
|
+
* user-driven update changes the normalized pair, with
|
|
452
|
+
* `{ start: number, end: number }` in `detail`.
|
|
139
453
|
*/
|
|
140
|
-
#commit(start, end, notify) {
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
let nextStart =
|
|
144
|
-
let nextEnd =
|
|
454
|
+
#commit(start, end, moving, notify) {
|
|
455
|
+
const range = this.#effectiveRange;
|
|
456
|
+
const previous = this.#currentPair(range);
|
|
457
|
+
let nextStart = snapSteppedValue(start, range);
|
|
458
|
+
let nextEnd = snapSteppedValue(end, range);
|
|
145
459
|
if (nextStart > nextEnd) {
|
|
146
|
-
if (
|
|
147
|
-
else nextEnd = nextStart;
|
|
460
|
+
if (moving === "start") nextStart = nextEnd;
|
|
461
|
+
else if (moving === "end") nextEnd = nextStart;
|
|
462
|
+
else [nextStart, nextEnd] = [nextEnd, nextStart];
|
|
148
463
|
}
|
|
149
|
-
this.startValue = nextStart;
|
|
150
|
-
this.endValue = nextEnd;
|
|
151
|
-
|
|
152
|
-
|
|
464
|
+
if (!Object.is(this.startValue, nextStart)) this.startValue = nextStart;
|
|
465
|
+
if (!Object.is(this.endValue, nextEnd)) this.endValue = nextEnd;
|
|
466
|
+
const pair = { start: nextStart, end: nextEnd };
|
|
467
|
+
this.#renderPair(pair, range);
|
|
468
|
+
if (notify && (nextStart !== previous.start || nextEnd !== previous.end)) {
|
|
153
469
|
this.dispatch("change", { detail: { start: nextStart, end: nextEnd } });
|
|
154
470
|
}
|
|
155
471
|
}
|
|
156
|
-
/**
|
|
157
|
-
|
|
472
|
+
/**
|
|
473
|
+
* Reflects morph-supplied Values without writing them back or dispatching.
|
|
474
|
+
*
|
|
475
|
+
* @stimeoRenderRoot
|
|
476
|
+
*/
|
|
477
|
+
#render() {
|
|
478
|
+
const range = this.#effectiveRange;
|
|
479
|
+
this.#renderPair(this.#currentPair(range), range);
|
|
480
|
+
}
|
|
481
|
+
/** Reflects one normalized pair onto both thumbs and CSS properties. */
|
|
482
|
+
#renderPair(pair, range) {
|
|
158
483
|
if (this.hasStartThumbTarget) {
|
|
159
|
-
this.startThumbTarget
|
|
160
|
-
this.startThumbTarget.setAttribute("aria-valuemax", String(end));
|
|
161
|
-
this.startThumbTarget.setAttribute("aria-valuenow", String(start));
|
|
484
|
+
this.#renderStartThumb(this.startThumbTarget, pair, range);
|
|
162
485
|
}
|
|
163
486
|
if (this.hasEndThumbTarget) {
|
|
164
|
-
this.endThumbTarget
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
487
|
+
this.#renderEndThumb(this.endThumbTarget, pair, range);
|
|
488
|
+
}
|
|
489
|
+
this.#renderFractions(pair, range);
|
|
490
|
+
}
|
|
491
|
+
/** Writes only changed ARIA for the lower thumb. */
|
|
492
|
+
#renderStartThumb(thumb, pair, range) {
|
|
493
|
+
this.#setAria(thumb, "aria-valuemin", range.min);
|
|
494
|
+
this.#setAria(thumb, "aria-valuemax", pair.end);
|
|
495
|
+
this.#setAria(thumb, "aria-valuenow", pair.start);
|
|
496
|
+
}
|
|
497
|
+
/** Writes only changed ARIA for the upper thumb. */
|
|
498
|
+
#renderEndThumb(thumb, pair, range) {
|
|
499
|
+
this.#setAria(thumb, "aria-valuemin", pair.start);
|
|
500
|
+
this.#setAria(thumb, "aria-valuemax", range.max);
|
|
501
|
+
this.#setAria(thumb, "aria-valuenow", pair.end);
|
|
502
|
+
}
|
|
503
|
+
/** Writes one numeric ARIA attribute only when its serialized value changed. */
|
|
504
|
+
#setAria(thumb, name, value) {
|
|
505
|
+
const next = String(value);
|
|
506
|
+
if (thumb.getAttribute(name) !== next) thumb.setAttribute(name, next);
|
|
507
|
+
}
|
|
508
|
+
/** Writes the two behavior-only fraction hooks only when they changed. */
|
|
509
|
+
#renderFractions(pair, range) {
|
|
510
|
+
const start = String(rangeFraction(pair.start, range.min, range.max));
|
|
511
|
+
const end = String(rangeFraction(pair.end, range.min, range.max));
|
|
512
|
+
if (this.element.style.getPropertyValue(START_PROPERTY) !== start) {
|
|
513
|
+
this.element.style.setProperty(START_PROPERTY, start);
|
|
514
|
+
}
|
|
515
|
+
if (this.element.style.getPropertyValue(END_PROPERTY) !== end) {
|
|
516
|
+
this.element.style.setProperty(END_PROPERTY, end);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/** Current normalized and ordered pair derived from live declarative Values. */
|
|
520
|
+
#currentPair(range) {
|
|
521
|
+
const rawStart = Number.isFinite(this.startValue) ? this.startValue : range.min;
|
|
522
|
+
const rawEnd = Number.isFinite(this.endValue) ? this.endValue : range.max;
|
|
523
|
+
const start = snapSteppedValue(rawStart, range);
|
|
524
|
+
const end = snapSteppedValue(rawEnd, range);
|
|
525
|
+
if (start <= end) return { start, end };
|
|
526
|
+
return { start: end, end: start };
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Finite ordered range used by ARIA, keyboard, pointer, and CSS reflection.
|
|
530
|
+
* Invalid endpoints fall back to the public defaults; an authored `max`
|
|
531
|
+
* below `min` collapses to the finite minimum.
|
|
532
|
+
*/
|
|
533
|
+
get #effectiveRange() {
|
|
534
|
+
const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;
|
|
535
|
+
const authoredMax = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;
|
|
536
|
+
return { min, max: Math.max(min, authoredMax), step: this.stepValue, base: min };
|
|
537
|
+
}
|
|
538
|
+
/** Ends the current pointer session without dispatching another change. */
|
|
539
|
+
#endDrag() {
|
|
540
|
+
const drag = this.#drag;
|
|
541
|
+
this.#drag = null;
|
|
542
|
+
drag?.pointer?.end();
|
|
184
543
|
}
|
|
185
544
|
};
|
|
186
|
-
function isUserMovingStart(start, prevStart, end, prevEnd) {
|
|
187
|
-
return start !== prevStart && end === prevEnd;
|
|
188
|
-
}
|
|
189
545
|
|
|
190
546
|
export { RangeSliderController };
|
|
191
547
|
//# sourceMappingURL=range_slider_controller.js.map
|