stimeo-ui 0.4.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 +68 -0
- 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 +6 -2
- data/dist/controllers/context_menu_controller.js +2 -2
- data/dist/controllers/countdown_controller.js +5 -1
- data/dist/controllers/date_range_picker_controller.js +5 -1
- data/dist/controllers/direct_upload_controller.js +3 -3
- data/dist/controllers/empty_state_controller.js +107 -16
- data/dist/controllers/flash_controller.js +161 -21
- data/dist/controllers/form_validation_controller.js +8 -2
- data/dist/controllers/frame_loading_controller.js +94 -22
- data/dist/controllers/highlight_controller.js +38 -1
- data/dist/controllers/idle_controller.js +13 -2
- data/dist/controllers/local_time_controller.js +2 -0
- data/dist/controllers/masonry_controller.js +1 -1
- data/dist/controllers/meter_controller.js +3 -1
- data/dist/controllers/network_status_controller.js +1 -3
- data/dist/controllers/number_input_controller.js +191 -24
- data/dist/controllers/overflow_menu_controller.js +1 -1
- 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 +7 -3
- data/dist/controllers/range_slider_controller.js +385 -94
- data/dist/controllers/rating_controller.js +2 -0
- data/dist/controllers/relative_time_controller.js +2 -0
- data/dist/controllers/scroll_area_controller.js +1 -1
- data/dist/controllers/separator_controller.js +13 -17
- data/dist/controllers/skeleton_controller.js +71 -3
- data/dist/controllers/slider_controller.js +325 -48
- data/dist/controllers/spinner_controller.js +18 -3
- data/dist/controllers/step_indicator_controller.js +3 -1
- data/dist/controllers/stepper_controller.js +2 -0
- 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 +1214 -307
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -51,6 +51,59 @@ var MicrotaskCoalescer = class {
|
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
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
|
+
|
|
54
107
|
// src/utils/range.ts
|
|
55
108
|
function rangeFraction(value, min, max) {
|
|
56
109
|
const span = max - min;
|
|
@@ -67,14 +120,122 @@ function rangeFraction(value, min, max) {
|
|
|
67
120
|
return Math.min(1, Math.max(0, fraction));
|
|
68
121
|
}
|
|
69
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
|
+
|
|
70
229
|
// src/controllers/range_slider_controller.ts
|
|
71
|
-
var START_PROPERTY = "--stimeo
|
|
72
|
-
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;
|
|
73
234
|
var RangeSliderController = class extends Controller {
|
|
74
235
|
static targets = ["track", "startThumb", "endThumb"];
|
|
75
236
|
static values = {
|
|
76
|
-
min: { type: Number, default:
|
|
77
|
-
max: { type: Number, default:
|
|
237
|
+
min: { type: Number, default: DEFAULT_MIN },
|
|
238
|
+
max: { type: Number, default: DEFAULT_MAX },
|
|
78
239
|
step: { type: Number, default: 1 },
|
|
79
240
|
start: { type: Number, default: 0 },
|
|
80
241
|
end: { type: Number, default: 100 },
|
|
@@ -82,31 +243,27 @@ var RangeSliderController = class extends Controller {
|
|
|
82
243
|
};
|
|
83
244
|
static actions = ["onKeydown", "onPointerDown"];
|
|
84
245
|
static events = ["change"];
|
|
85
|
-
/**
|
|
86
|
-
#
|
|
246
|
+
/** One initiating pointer owns each live drag and its stable target snapshot. */
|
|
247
|
+
#drag = null;
|
|
87
248
|
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
88
249
|
get #mirrored() {
|
|
89
250
|
return this.logicalTrackValue && isRtl(this.element);
|
|
90
251
|
}
|
|
91
|
-
/** Normalizes the initial pair (clamped, snapped, ordered) and renders. */
|
|
92
252
|
/**
|
|
93
253
|
* Collapses a morph that swaps render inputs into one repaint, and refuses the
|
|
94
254
|
* pass Stimulus delivers before `connect()`.
|
|
95
255
|
*/
|
|
96
|
-
#repaint = new MicrotaskCoalescer(() =>
|
|
97
|
-
this.#render(this.startValue, this.endValue);
|
|
98
|
-
});
|
|
256
|
+
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
99
257
|
connect() {
|
|
100
258
|
this.#repaint.activate();
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
this.#commit(
|
|
259
|
+
const range = this.#effectiveRange;
|
|
260
|
+
const pair = this.#currentPair(range);
|
|
261
|
+
this.#commit(pair.start, pair.end, null, false);
|
|
104
262
|
}
|
|
105
263
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
106
264
|
disconnect() {
|
|
107
265
|
this.#repaint.cancel();
|
|
108
|
-
this.#
|
|
109
|
-
this.#dragAbort = null;
|
|
266
|
+
this.#endDrag();
|
|
110
267
|
}
|
|
111
268
|
/** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
|
|
112
269
|
minValueChanged() {
|
|
@@ -116,141 +273,275 @@ var RangeSliderController = class extends Controller {
|
|
|
116
273
|
maxValueChanged() {
|
|
117
274
|
this.#repaint.schedule();
|
|
118
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();
|
|
323
|
+
}
|
|
119
324
|
/** Keyboard stepping for whichever thumb is focused (the action's element). */
|
|
120
325
|
onKeydown(event) {
|
|
121
326
|
if (isReservedArrowChord(event)) return;
|
|
122
327
|
const thumb = event.currentTarget;
|
|
123
328
|
const isStart = this.hasStartThumbTarget && thumb === this.startThumbTarget;
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
+
}
|
|
128
355
|
let next = null;
|
|
129
356
|
switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {
|
|
130
357
|
case "ArrowRight":
|
|
131
358
|
case "ArrowUp":
|
|
132
|
-
next = current
|
|
359
|
+
next = stepSteppedValue(current, 1, range);
|
|
133
360
|
break;
|
|
134
361
|
case "ArrowLeft":
|
|
135
362
|
case "ArrowDown":
|
|
136
|
-
next = current -
|
|
363
|
+
next = stepSteppedValue(current, -1, range);
|
|
137
364
|
break;
|
|
138
365
|
case "PageUp":
|
|
139
|
-
next = current
|
|
366
|
+
next = stepSteppedValue(current, 10, range);
|
|
140
367
|
break;
|
|
141
368
|
case "PageDown":
|
|
142
|
-
next = current -
|
|
369
|
+
next = stepSteppedValue(current, -10, range);
|
|
143
370
|
break;
|
|
144
371
|
case "Home":
|
|
145
|
-
next =
|
|
372
|
+
next = range.min;
|
|
146
373
|
break;
|
|
147
374
|
case "End":
|
|
148
|
-
next =
|
|
375
|
+
next = range.max;
|
|
149
376
|
break;
|
|
150
377
|
default:
|
|
151
378
|
return;
|
|
152
379
|
}
|
|
153
380
|
event.preventDefault();
|
|
154
|
-
this.#moveThumb(
|
|
381
|
+
this.#moveThumb(kind, next);
|
|
155
382
|
}
|
|
156
383
|
/** Begins a pointer drag on the track, moving the thumb nearest the press. */
|
|
157
384
|
onPointerDown(event) {
|
|
158
|
-
if (!this.hasTrackTarget)
|
|
385
|
+
if (event.button !== 0 || this.#drag || !this.hasTrackTarget || !this.hasStartThumbTarget || !this.hasEndThumbTarget) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const track = this.trackTarget;
|
|
159
389
|
const mirrored = this.#mirrored;
|
|
160
|
-
const value = this.#valueFromClientX(event.clientX, mirrored);
|
|
390
|
+
const value = this.#valueFromClientX(event.clientX, mirrored, track);
|
|
161
391
|
if (value === null) return;
|
|
162
392
|
event.preventDefault();
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
184
415
|
}
|
|
185
416
|
/** Maps a pointer X coordinate to a raw value using the track geometry. */
|
|
186
|
-
#valueFromClientX(clientX, mirrored) {
|
|
187
|
-
const rect =
|
|
417
|
+
#valueFromClientX(clientX, mirrored, track) {
|
|
418
|
+
const rect = track.getBoundingClientRect();
|
|
188
419
|
if (rect.width === 0) return null;
|
|
189
420
|
const offset = (clientX - rect.left) / rect.width;
|
|
190
421
|
const fraction = mirrored ? 1 - offset : offset;
|
|
191
|
-
|
|
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";
|
|
192
437
|
}
|
|
193
438
|
/** Moves one thumb to a new raw value, keeping the pair ordered. */
|
|
194
|
-
#moveThumb(
|
|
195
|
-
|
|
196
|
-
|
|
439
|
+
#moveThumb(kind, raw) {
|
|
440
|
+
const pair = this.#currentPair(this.#effectiveRange);
|
|
441
|
+
if (kind === "start") {
|
|
442
|
+
this.#commit(raw, pair.end, "start", true);
|
|
197
443
|
} else {
|
|
198
|
-
this.#commit(
|
|
444
|
+
this.#commit(pair.start, raw, "end", true);
|
|
199
445
|
}
|
|
200
446
|
}
|
|
201
447
|
/**
|
|
202
448
|
* Clamps and snaps `start`/`end`, enforces `start ≤ end`, stores the pair, and
|
|
203
449
|
* reflects it onto the thumbs' ARIA attributes and the range custom
|
|
204
|
-
* 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`.
|
|
205
453
|
*/
|
|
206
|
-
#commit(start, end, notify) {
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
let nextStart =
|
|
210
|
-
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);
|
|
211
459
|
if (nextStart > nextEnd) {
|
|
212
|
-
if (
|
|
213
|
-
else nextEnd = nextStart;
|
|
460
|
+
if (moving === "start") nextStart = nextEnd;
|
|
461
|
+
else if (moving === "end") nextEnd = nextStart;
|
|
462
|
+
else [nextStart, nextEnd] = [nextEnd, nextStart];
|
|
214
463
|
}
|
|
215
|
-
this.startValue = nextStart;
|
|
216
|
-
this.endValue = nextEnd;
|
|
217
|
-
|
|
218
|
-
|
|
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)) {
|
|
219
469
|
this.dispatch("change", { detail: { start: nextStart, end: nextEnd } });
|
|
220
470
|
}
|
|
221
471
|
}
|
|
222
|
-
/**
|
|
223
|
-
|
|
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) {
|
|
224
483
|
if (this.hasStartThumbTarget) {
|
|
225
|
-
this.startThumbTarget
|
|
226
|
-
this.startThumbTarget.setAttribute("aria-valuemax", String(end));
|
|
227
|
-
this.startThumbTarget.setAttribute("aria-valuenow", String(start));
|
|
484
|
+
this.#renderStartThumb(this.startThumbTarget, pair, range);
|
|
228
485
|
}
|
|
229
486
|
if (this.hasEndThumbTarget) {
|
|
230
|
-
this.endThumbTarget
|
|
231
|
-
|
|
232
|
-
|
|
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);
|
|
233
517
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
);
|
|
238
|
-
this.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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();
|
|
249
543
|
}
|
|
250
544
|
};
|
|
251
|
-
function isUserMovingStart(start, prevStart, end, prevEnd) {
|
|
252
|
-
return start !== prevStart && end === prevEnd;
|
|
253
|
-
}
|
|
254
545
|
|
|
255
546
|
export { RangeSliderController };
|
|
256
547
|
//# sourceMappingURL=range_slider_controller.js.map
|
|
@@ -175,6 +175,8 @@ var RatingController = class extends Controller {
|
|
|
175
175
|
* Applies `value` (already clamped) everywhere, then dispatches `change`.
|
|
176
176
|
* Use for user-driven changes; on connect call `#apply` directly so
|
|
177
177
|
* initialization mirrors state without emitting an event.
|
|
178
|
+
*
|
|
179
|
+
* @stimeoRenderRoot
|
|
178
180
|
*/
|
|
179
181
|
#render(value, { focus }) {
|
|
180
182
|
this.#apply(value, { focus });
|
|
@@ -208,6 +208,8 @@ var RelativeTimeController = class extends Controller {
|
|
|
208
208
|
* polling can stop: a *past* timestamp that fell back to the absolute text can
|
|
209
209
|
* never leave it again, and a locale the runtime rejects has nothing to render
|
|
210
210
|
* until that value is corrected.
|
|
211
|
+
*
|
|
212
|
+
* @stimeoRenderRoot
|
|
211
213
|
*/
|
|
212
214
|
#applyAndComputeDelay() {
|
|
213
215
|
const deltaMs = this.#targetMs - Date.now();
|
|
@@ -165,7 +165,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
165
165
|
this.#syncKeyboardReach(vp, overflowing);
|
|
166
166
|
const { position, progress } = this.#measurePosition(vp);
|
|
167
167
|
this.element.setAttribute("data-scroll", position);
|
|
168
|
-
this.element.style.setProperty("--stimeo
|
|
168
|
+
this.element.style.setProperty("--stimeo--scroll-progress", String(progress));
|
|
169
169
|
const edge = position === "start" ? "start" : position === "end" ? "end" : null;
|
|
170
170
|
if (overflowing && edge && edge !== this.#lastEdge) {
|
|
171
171
|
this.#lastEdge = edge;
|
|
@@ -8,6 +8,13 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
8
8
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// src/utils/default_attribute.ts
|
|
12
|
+
function setDefaultAttribute(element, name, value) {
|
|
13
|
+
if (element.hasAttribute(name)) return false;
|
|
14
|
+
element.setAttribute(name, value);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
// src/controllers/separator_controller.ts
|
|
12
19
|
var SeparatorController = class extends Controller {
|
|
13
20
|
static values = {
|
|
@@ -18,19 +25,13 @@ var SeparatorController = class extends Controller {
|
|
|
18
25
|
static actions = ["onKeydown"];
|
|
19
26
|
static events = ["change"];
|
|
20
27
|
connect() {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
if (!this.element.hasAttribute("aria-orientation")) {
|
|
25
|
-
this.element.setAttribute("aria-orientation", this.orientationValue);
|
|
26
|
-
}
|
|
28
|
+
setDefaultAttribute(this.element, "role", "separator");
|
|
29
|
+
setDefaultAttribute(this.element, "aria-orientation", this.orientationValue);
|
|
27
30
|
if (this.focusableValue) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
this
|
|
32
|
-
this.#setDefault("aria-valuemax", "100");
|
|
33
|
-
this.#setDefault("aria-valuenow", String(this.#clamp(this.#value)));
|
|
31
|
+
setDefaultAttribute(this.element, "tabindex", "0");
|
|
32
|
+
setDefaultAttribute(this.element, "aria-valuemin", "0");
|
|
33
|
+
setDefaultAttribute(this.element, "aria-valuemax", "100");
|
|
34
|
+
setDefaultAttribute(this.element, "aria-valuenow", String(this.#clamp(this.#value)));
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
/** Adjusts the value on arrow / Home / End keys (focusable variant only). */
|
|
@@ -84,11 +85,6 @@ var SeparatorController = class extends Controller {
|
|
|
84
85
|
const parsed = Number.parseFloat(this.element.getAttribute(name) ?? "");
|
|
85
86
|
return Number.isNaN(parsed) ? fallback : parsed;
|
|
86
87
|
}
|
|
87
|
-
#setDefault(name, value) {
|
|
88
|
-
if (!this.element.hasAttribute(name)) {
|
|
89
|
-
this.element.setAttribute(name, value);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
88
|
};
|
|
93
89
|
|
|
94
90
|
export { SeparatorController };
|