stimeo-ui 0.2.0 → 0.2.1
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 +59 -0
- data/dist/controllers/alert_dialog_controller.js +318 -0
- data/dist/controllers/carousel_controller.js +272 -0
- data/dist/controllers/clipboard_controller.js +144 -0
- data/dist/controllers/collapsible_controller.js +327 -0
- data/dist/controllers/color_picker_controller.js +213 -0
- data/dist/controllers/count_up_controller.js +8 -1
- data/dist/controllers/currency_input_controller.js +147 -0
- data/dist/controllers/data_grid_controller.js +168 -0
- data/dist/controllers/date_range_picker_controller.js +417 -0
- data/dist/controllers/dismissible_controller.js +117 -0
- data/dist/controllers/drawer_controller.js +630 -0
- data/dist/controllers/editable_controller.js +168 -0
- data/dist/controllers/file_dropzone_controller.js +165 -0
- data/dist/controllers/filter_controller.js +86 -0
- data/dist/controllers/flash_controller.js +36 -5
- data/dist/controllers/highlight_controller.js +6 -4
- data/dist/controllers/intersection_controller.js +41 -18
- data/dist/controllers/lazy_frame_controller.js +33 -11
- data/dist/controllers/masonry_controller.js +142 -0
- data/dist/controllers/menubar_controller.js +433 -0
- data/dist/controllers/multi_select_controller.js +472 -0
- data/dist/controllers/navigation_menu_controller.js +384 -0
- data/dist/controllers/overflow_indicator_controller.js +178 -27
- data/dist/controllers/password_reveal_controller.js +117 -0
- data/dist/controllers/range_slider_controller.js +166 -0
- data/dist/controllers/read_more_controller.js +194 -0
- data/dist/controllers/scroll_area_controller.js +15 -2
- data/dist/controllers/scroll_restore_controller.js +93 -0
- data/dist/controllers/scroll_visibility_controller.js +8 -4
- data/dist/controllers/scrollspy_controller.js +33 -11
- data/dist/controllers/separator_controller.js +87 -0
- data/dist/controllers/sidebar_controller.js +761 -0
- data/dist/controllers/stepper_controller.js +28 -12
- data/dist/controllers/stick_to_bottom_controller.js +8 -4
- data/dist/controllers/sticky_observer_controller.js +88 -20
- data/dist/controllers/tags_input_controller.js +275 -0
- data/dist/controllers/theme_controller.js +20 -10
- data/dist/controllers/time_picker_controller.js +212 -0
- data/dist/controllers/toast_controller.js +36 -9
- data/dist/controllers/transition_controller.js +153 -38
- data/dist/controllers/tree_view_controller.js +275 -0
- data/dist/index.js +811 -295
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +28 -2
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/time_picker_controller.ts
|
|
4
|
+
var AM = 0;
|
|
5
|
+
var PM = 1;
|
|
6
|
+
var TimePickerController = class extends Controller {
|
|
7
|
+
static targets = ["segment", "field"];
|
|
8
|
+
static values = {
|
|
9
|
+
hourCycle: { type: Number, default: 24 },
|
|
10
|
+
step: { type: Number, default: 1 },
|
|
11
|
+
seconds: { type: Boolean, default: false },
|
|
12
|
+
wrap: { type: Boolean, default: true }
|
|
13
|
+
};
|
|
14
|
+
static actions = ["onKeydown"];
|
|
15
|
+
static events = ["change"];
|
|
16
|
+
/** Current numeric value per segment kind (hours are the *displayed* hours). */
|
|
17
|
+
#state = { hour: 0, minute: 0, second: 0, meridiem: AM };
|
|
18
|
+
/** Direct-entry digit buffer and the segment it belongs to. */
|
|
19
|
+
#typeBuffer = "";
|
|
20
|
+
#typeSegment = null;
|
|
21
|
+
/** Last composed field value, to suppress duplicate `change` dispatches. */
|
|
22
|
+
#lastValue = "";
|
|
23
|
+
/** Seeds each segment from its initial `aria-valuenow` and syncs the field. */
|
|
24
|
+
connect() {
|
|
25
|
+
for (const segment of this.segmentTargets) {
|
|
26
|
+
const kind = this.#kindOf(segment);
|
|
27
|
+
if (!kind) continue;
|
|
28
|
+
const now = Number(segment.getAttribute("aria-valuenow"));
|
|
29
|
+
const { min, max } = this.#bounds(kind);
|
|
30
|
+
this.#state[kind] = Number.isFinite(now) ? Math.min(max, Math.max(min, now)) : min;
|
|
31
|
+
}
|
|
32
|
+
for (const segment of this.segmentTargets) this.#renderSegment(segment);
|
|
33
|
+
this.#syncField(false);
|
|
34
|
+
}
|
|
35
|
+
/** Handles stepping, inter-segment focus moves, jumps, and direct entry. */
|
|
36
|
+
onKeydown(event) {
|
|
37
|
+
const segment = event.target?.closest(
|
|
38
|
+
"[data-stimeo--time-picker-target='segment']"
|
|
39
|
+
);
|
|
40
|
+
const kind = segment ? this.#kindOf(segment) : null;
|
|
41
|
+
if (!segment || !kind) return;
|
|
42
|
+
switch (event.key) {
|
|
43
|
+
case "ArrowUp":
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
this.#step(kind, this.#delta(kind));
|
|
46
|
+
break;
|
|
47
|
+
case "ArrowDown":
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
this.#step(kind, -this.#delta(kind));
|
|
50
|
+
break;
|
|
51
|
+
case "ArrowLeft":
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
this.#focusSibling(segment, -1);
|
|
54
|
+
break;
|
|
55
|
+
case "ArrowRight":
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
this.#focusSibling(segment, 1);
|
|
58
|
+
break;
|
|
59
|
+
case "Home":
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
this.#set(kind, this.#bounds(kind).min);
|
|
62
|
+
break;
|
|
63
|
+
case "End":
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
this.#set(kind, this.#bounds(kind).max);
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
if (/^[0-9]$/.test(event.key)) {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
this.#typeDigit(segment, kind, event.key);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.#typeBuffer = "";
|
|
75
|
+
this.#typeSegment = null;
|
|
76
|
+
}
|
|
77
|
+
/** The per-step delta: minutes step by `step`, others by 1, meridiem toggles. */
|
|
78
|
+
#delta(kind) {
|
|
79
|
+
return kind === "minute" ? this.stepValue : 1;
|
|
80
|
+
}
|
|
81
|
+
/** Steps a segment, wrapping at its bounds and carrying over when enabled. */
|
|
82
|
+
#step(kind, delta) {
|
|
83
|
+
if (kind === "meridiem") {
|
|
84
|
+
this.#set("meridiem", this.#state.meridiem === AM ? PM : AM);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const { min, max } = this.#bounds(kind);
|
|
88
|
+
const span = max - min + 1;
|
|
89
|
+
const raw = this.#state[kind] + delta;
|
|
90
|
+
if (raw > max || raw < min) {
|
|
91
|
+
if (!this.wrapValue) {
|
|
92
|
+
this.#set(kind, Math.min(max, Math.max(min, raw)));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const wrapped = ((raw - min) % span + span) % span + min;
|
|
96
|
+
const carry = Math.floor((raw - min) / span);
|
|
97
|
+
this.#set(kind, wrapped);
|
|
98
|
+
this.#carry(kind, carry);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.#set(kind, raw);
|
|
102
|
+
}
|
|
103
|
+
/** Propagates a wrap carry from `kind` into the next larger segment. */
|
|
104
|
+
#carry(kind, amount) {
|
|
105
|
+
if (amount === 0) return;
|
|
106
|
+
if (kind === "second") this.#step("minute", amount);
|
|
107
|
+
else if (kind === "minute") this.#step("hour", amount);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Sets a segment's value (clamped to its `[min, max]` bounds), re-renders it,
|
|
111
|
+
* and resyncs the composed field. Clamping here guards the direct-entry path:
|
|
112
|
+
* typing `0` into a 12-hour hour (min 1) must not commit an out-of-range
|
|
113
|
+
* `aria-valuenow="0"`. The stepping path already passes in-bounds values, so
|
|
114
|
+
* the clamp is a no-op there.
|
|
115
|
+
*/
|
|
116
|
+
#set(kind, value) {
|
|
117
|
+
const { min, max } = this.#bounds(kind);
|
|
118
|
+
this.#state[kind] = Math.min(max, Math.max(min, value));
|
|
119
|
+
const segment = this.segmentTargets.find((s) => this.#kindOf(s) === kind);
|
|
120
|
+
if (segment) this.#renderSegment(segment);
|
|
121
|
+
this.#syncField(true);
|
|
122
|
+
}
|
|
123
|
+
/** Accumulates a typed digit, committing and advancing after two digits. */
|
|
124
|
+
#typeDigit(segment, kind, digit) {
|
|
125
|
+
if (kind === "meridiem") return;
|
|
126
|
+
if (this.#typeSegment !== kind) this.#typeBuffer = "";
|
|
127
|
+
this.#typeSegment = kind;
|
|
128
|
+
const { max } = this.#bounds(kind);
|
|
129
|
+
const candidate = Number(`${this.#typeBuffer}${digit}`);
|
|
130
|
+
if (candidate <= max) this.#typeBuffer = `${this.#typeBuffer}${digit}`;
|
|
131
|
+
else this.#typeBuffer = digit;
|
|
132
|
+
this.#set(kind, Number(this.#typeBuffer));
|
|
133
|
+
if (this.#typeBuffer.length >= 2 || Number(this.#typeBuffer) * 10 > max) {
|
|
134
|
+
this.#typeBuffer = "";
|
|
135
|
+
this.#typeSegment = null;
|
|
136
|
+
this.#focusSibling(segment, 1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Moves focus to the previous/next segment, if one exists. */
|
|
140
|
+
#focusSibling(segment, direction) {
|
|
141
|
+
const index = this.segmentTargets.indexOf(segment);
|
|
142
|
+
const next = this.segmentTargets[index + direction];
|
|
143
|
+
next?.focus();
|
|
144
|
+
}
|
|
145
|
+
/** Reflects a segment's current value onto its ARIA/text representation. */
|
|
146
|
+
#renderSegment(segment) {
|
|
147
|
+
const kind = this.#kindOf(segment);
|
|
148
|
+
if (!kind) return;
|
|
149
|
+
const value = this.#state[kind];
|
|
150
|
+
if (kind === "meridiem") {
|
|
151
|
+
const text2 = value === PM ? "PM" : "AM";
|
|
152
|
+
segment.setAttribute("aria-valuenow", String(value));
|
|
153
|
+
segment.setAttribute("aria-valuetext", text2);
|
|
154
|
+
segment.setAttribute("aria-valuemin", String(AM));
|
|
155
|
+
segment.setAttribute("aria-valuemax", String(PM));
|
|
156
|
+
segment.textContent = text2;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const { min, max } = this.#bounds(kind);
|
|
160
|
+
const text = String(value).padStart(2, "0");
|
|
161
|
+
segment.setAttribute("aria-valuenow", String(value));
|
|
162
|
+
segment.setAttribute("aria-valuetext", text);
|
|
163
|
+
segment.setAttribute("aria-valuemin", String(min));
|
|
164
|
+
segment.setAttribute("aria-valuemax", String(max));
|
|
165
|
+
segment.textContent = text;
|
|
166
|
+
}
|
|
167
|
+
/** Composes `HH:MM[:SS]` (24-hour) into the hidden field; dispatches `change`. */
|
|
168
|
+
#syncField(notify) {
|
|
169
|
+
const h24 = this.#hours24();
|
|
170
|
+
const parts = [pad(h24), pad(this.#state.minute)];
|
|
171
|
+
if (this.secondsValue) parts.push(pad(this.#state.second));
|
|
172
|
+
const value = parts.join(":");
|
|
173
|
+
if (this.hasFieldTarget && this.fieldTarget.value !== value) {
|
|
174
|
+
this.fieldTarget.value = value;
|
|
175
|
+
}
|
|
176
|
+
if (notify && value !== this.#lastValue) this.dispatch("change", { detail: { value } });
|
|
177
|
+
this.#lastValue = value;
|
|
178
|
+
}
|
|
179
|
+
/** Converts the displayed hour (+ meridiem in 12-hour mode) to 24-hour. */
|
|
180
|
+
#hours24() {
|
|
181
|
+
if (this.hourCycleValue !== 12) return this.#state.hour;
|
|
182
|
+
const base = this.#state.hour % 12;
|
|
183
|
+
return base + (this.#state.meridiem === PM ? 12 : 0);
|
|
184
|
+
}
|
|
185
|
+
/** The inclusive `[min, max]` bounds for a segment kind. */
|
|
186
|
+
#bounds(kind) {
|
|
187
|
+
switch (kind) {
|
|
188
|
+
case "hour":
|
|
189
|
+
return this.hourCycleValue === 12 ? { min: 1, max: 12 } : { min: 0, max: 23 };
|
|
190
|
+
case "minute":
|
|
191
|
+
case "second":
|
|
192
|
+
return { min: 0, max: 59 };
|
|
193
|
+
case "meridiem":
|
|
194
|
+
return { min: AM, max: PM };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Reads a segment's declared kind, or null when absent/invalid. */
|
|
198
|
+
#kindOf(segment) {
|
|
199
|
+
const kind = segment.getAttribute("data-segment");
|
|
200
|
+
if (kind === "hour" || kind === "minute" || kind === "second" || kind === "meridiem") {
|
|
201
|
+
return kind;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
function pad(value) {
|
|
207
|
+
return String(value).padStart(2, "0");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { TimePickerController };
|
|
211
|
+
//# sourceMappingURL=time_picker_controller.js.map
|
|
212
|
+
//# sourceMappingURL=time_picker_controller.js.map
|
|
@@ -55,6 +55,40 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
// src/utils/transition_completion.ts
|
|
59
|
+
function timeMs(value) {
|
|
60
|
+
const trimmed = value.trim();
|
|
61
|
+
const amount = Number.parseFloat(trimmed);
|
|
62
|
+
if (!Number.isFinite(amount)) return 0;
|
|
63
|
+
if (trimmed.endsWith("ms")) return amount;
|
|
64
|
+
if (trimmed.endsWith("s")) return amount * 1e3;
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
function cssList(value) {
|
|
68
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
function transitionTimings(style) {
|
|
71
|
+
const properties = cssList(style.transitionProperty);
|
|
72
|
+
const durations = cssList(style.transitionDuration).map(timeMs);
|
|
73
|
+
const delays = cssList(style.transitionDelay).map(timeMs);
|
|
74
|
+
const effectiveProperties = properties.length > 0 ? properties : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => "all");
|
|
75
|
+
const effectiveDurations = durations.length > 0 ? durations : [0];
|
|
76
|
+
const effectiveDelays = delays.length > 0 ? delays : [0];
|
|
77
|
+
return effectiveProperties.filter((property) => property !== "none").map((property, index) => ({
|
|
78
|
+
property,
|
|
79
|
+
totalMs: Math.max(
|
|
80
|
+
0,
|
|
81
|
+
(effectiveDurations[index % effectiveDurations.length] ?? 0) + (effectiveDelays[index % effectiveDelays.length] ?? 0)
|
|
82
|
+
)
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
function maxTotalMs(timings) {
|
|
86
|
+
return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);
|
|
87
|
+
}
|
|
88
|
+
function maxTransitionTotalMs(style) {
|
|
89
|
+
return maxTotalMs(transitionTimings(style));
|
|
90
|
+
}
|
|
91
|
+
|
|
58
92
|
// src/controllers/toast_controller.ts
|
|
59
93
|
var DELEGATED_EVENTS = ["click", "focusin", "focusout", "keydown", "mouseover", "mouseout"];
|
|
60
94
|
var ToastController = class extends Controller {
|
|
@@ -304,8 +338,7 @@ var ToastController = class extends Controller {
|
|
|
304
338
|
this.listTarget.removeChild(element);
|
|
305
339
|
this.dispatch("dismiss", { detail: { item: element, reason } });
|
|
306
340
|
};
|
|
307
|
-
const
|
|
308
|
-
const duration = cssTimeToMs(transitions);
|
|
341
|
+
const duration = maxTransitionTotalMs(window.getComputedStyle(element));
|
|
309
342
|
if (duration > 0) {
|
|
310
343
|
this.#timers.set(finalize, duration);
|
|
311
344
|
} else {
|
|
@@ -377,13 +410,7 @@ var ToastController = class extends Controller {
|
|
|
377
410
|
this.#rafHandles.delete(element);
|
|
378
411
|
}
|
|
379
412
|
};
|
|
380
|
-
function cssTimeToMs(value) {
|
|
381
|
-
const first = value.split(",")[0]?.trim() ?? "";
|
|
382
|
-
const amount = Number.parseFloat(first);
|
|
383
|
-
if (Number.isNaN(amount)) return 0;
|
|
384
|
-
return first.endsWith("ms") ? amount : amount * 1e3;
|
|
385
|
-
}
|
|
386
413
|
|
|
387
|
-
export { ToastController
|
|
414
|
+
export { ToastController };
|
|
388
415
|
//# sourceMappingURL=toast_controller.js.map
|
|
389
416
|
//# sourceMappingURL=toast_controller.js.map
|
|
@@ -2,6 +2,11 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/transition_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/reduced_motion.ts
|
|
6
|
+
function prefersReducedMotion() {
|
|
7
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
// src/utils/safe_timeout.ts
|
|
6
11
|
var TimerRegistry = class {
|
|
7
12
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -55,14 +60,149 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
55
60
|
}
|
|
56
61
|
};
|
|
57
62
|
|
|
63
|
+
// src/utils/transition_completion.ts
|
|
64
|
+
function timeMs(value) {
|
|
65
|
+
const trimmed = value.trim();
|
|
66
|
+
const amount = Number.parseFloat(trimmed);
|
|
67
|
+
if (!Number.isFinite(amount)) return 0;
|
|
68
|
+
if (trimmed.endsWith("ms")) return amount;
|
|
69
|
+
if (trimmed.endsWith("s")) return amount * 1e3;
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
function cssList(value) {
|
|
73
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
function transitionTimings(style) {
|
|
76
|
+
const properties = cssList(style.transitionProperty);
|
|
77
|
+
const durations = cssList(style.transitionDuration).map(timeMs);
|
|
78
|
+
const delays = cssList(style.transitionDelay).map(timeMs);
|
|
79
|
+
const effectiveProperties = properties.length > 0 ? properties : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => "all");
|
|
80
|
+
const effectiveDurations = durations.length > 0 ? durations : [0];
|
|
81
|
+
const effectiveDelays = delays.length > 0 ? delays : [0];
|
|
82
|
+
return effectiveProperties.filter((property) => property !== "none").map((property, index) => ({
|
|
83
|
+
property,
|
|
84
|
+
totalMs: Math.max(
|
|
85
|
+
0,
|
|
86
|
+
(effectiveDurations[index % effectiveDurations.length] ?? 0) + (effectiveDelays[index % effectiveDelays.length] ?? 0)
|
|
87
|
+
)
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
function maxTotalMs(timings) {
|
|
91
|
+
return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);
|
|
92
|
+
}
|
|
93
|
+
var TransitionCompletion = class {
|
|
94
|
+
#timers = new SafeTimeout();
|
|
95
|
+
#element = null;
|
|
96
|
+
#complete = null;
|
|
97
|
+
#pendingProperties = null;
|
|
98
|
+
#deadline = 0;
|
|
99
|
+
/**
|
|
100
|
+
* Replaces any prior wait and invokes `complete` synchronously for a 0ms
|
|
101
|
+
* transition (including when `getComputedStyle` is unavailable).
|
|
102
|
+
*
|
|
103
|
+
* With a positive `options.timeoutMs` the synchronous fast-path is skipped and
|
|
104
|
+
* the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).
|
|
105
|
+
*/
|
|
106
|
+
wait(element, complete, options = {}) {
|
|
107
|
+
this.cancel();
|
|
108
|
+
const requested = options.timeoutMs ?? 0;
|
|
109
|
+
const override = Number.isFinite(requested) && requested > 0 ? requested : 0;
|
|
110
|
+
const timings = typeof window.getComputedStyle === "function" ? transitionTimings(window.getComputedStyle(element)) : [];
|
|
111
|
+
const maximum = maxTotalMs(timings);
|
|
112
|
+
if (maximum <= 0 && override <= 0) {
|
|
113
|
+
complete();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.#element = element;
|
|
117
|
+
this.#complete = complete;
|
|
118
|
+
this.#deadline = Date.now() + maximum;
|
|
119
|
+
const activeProperties = this.#activeTransitionProperties(element);
|
|
120
|
+
this.#pendingProperties = activeProperties.size > 0 ? activeProperties : this.#explicitPendingProperties(timings);
|
|
121
|
+
element.addEventListener("transitionend", this.#onTerminal);
|
|
122
|
+
element.addEventListener("transitioncancel", this.#onTerminal);
|
|
123
|
+
this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);
|
|
124
|
+
}
|
|
125
|
+
/** Cancels the pending wait without invoking its completion callback. */
|
|
126
|
+
cancel() {
|
|
127
|
+
this.#complete = null;
|
|
128
|
+
this.#teardown();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Handles terminal events from the observed element only.
|
|
132
|
+
*
|
|
133
|
+
* For explicit property lists, every declared positive-time property must
|
|
134
|
+
* settle. For `all`, no reliable property set exists, so an event can finish
|
|
135
|
+
* only after the computed maximum time; the safety timeout owns the usual path.
|
|
136
|
+
*/
|
|
137
|
+
#onTerminal = (event) => {
|
|
138
|
+
if (event.target !== this.#element) return;
|
|
139
|
+
const transitionEvent = event;
|
|
140
|
+
if (transitionEvent.pseudoElement) return;
|
|
141
|
+
if (this.#pendingProperties) {
|
|
142
|
+
const propertyName = transitionEvent.propertyName;
|
|
143
|
+
const active = this.#activeTransitionProperties(this.#element);
|
|
144
|
+
if (active.has(propertyName)) return;
|
|
145
|
+
if (!this.#pendingProperties.delete(propertyName)) return;
|
|
146
|
+
if (this.#pendingProperties.size > 0) return;
|
|
147
|
+
if (active.size > 0) {
|
|
148
|
+
this.#pendingProperties = active;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.#finish();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (Date.now() >= this.#deadline) this.#finish();
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Returns active CSS transition properties expanded to the names reported by
|
|
158
|
+
* terminal events. CSS animations and pseudo-element effects are excluded.
|
|
159
|
+
*/
|
|
160
|
+
#activeTransitionProperties(element) {
|
|
161
|
+
if (!element || typeof element.getAnimations !== "function") return /* @__PURE__ */ new Set();
|
|
162
|
+
try {
|
|
163
|
+
const properties = element.getAnimations().flatMap((animation) => {
|
|
164
|
+
if (animation.playState === "idle" || animation.playState === "finished") return [];
|
|
165
|
+
const effect = animation.effect;
|
|
166
|
+
if (effect?.pseudoElement) return [];
|
|
167
|
+
const target = effect?.target;
|
|
168
|
+
if (target && target !== element) return [];
|
|
169
|
+
const property = animation.transitionProperty;
|
|
170
|
+
return typeof property === "string" && property.length > 0 ? [property] : [];
|
|
171
|
+
});
|
|
172
|
+
return new Set(properties);
|
|
173
|
+
} catch {
|
|
174
|
+
return /* @__PURE__ */ new Set();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */
|
|
178
|
+
#explicitPendingProperties(timings) {
|
|
179
|
+
if (timings.some(({ property }) => property === "all")) return null;
|
|
180
|
+
const pending = new Set(
|
|
181
|
+
timings.filter(({ totalMs }) => totalMs > 0).map(({ property }) => property)
|
|
182
|
+
);
|
|
183
|
+
return pending.size > 0 ? pending : null;
|
|
184
|
+
}
|
|
185
|
+
/** Completes exactly once, releasing listeners and the fallback before the callback. */
|
|
186
|
+
#finish() {
|
|
187
|
+
const complete = this.#complete;
|
|
188
|
+
if (!complete) return;
|
|
189
|
+
this.#complete = null;
|
|
190
|
+
this.#teardown();
|
|
191
|
+
complete();
|
|
192
|
+
}
|
|
193
|
+
/** Releases the exact element listeners and timer owned by the current wait. */
|
|
194
|
+
#teardown() {
|
|
195
|
+
this.#timers.clearAll();
|
|
196
|
+
this.#element?.removeEventListener("transitionend", this.#onTerminal);
|
|
197
|
+
this.#element?.removeEventListener("transitioncancel", this.#onTerminal);
|
|
198
|
+
this.#element = null;
|
|
199
|
+
this.#pendingProperties = null;
|
|
200
|
+
this.#deadline = 0;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
58
204
|
// src/controllers/transition_controller.ts
|
|
59
205
|
var tokensOf = (value) => value.split(/\s+/).filter(Boolean);
|
|
60
|
-
var firstTimeMs = (value) => {
|
|
61
|
-
const first = value.split(",")[0]?.trim() ?? "";
|
|
62
|
-
const amount = Number.parseFloat(first);
|
|
63
|
-
if (Number.isNaN(amount)) return 0;
|
|
64
|
-
return first.endsWith("ms") ? amount : amount * 1e3;
|
|
65
|
-
};
|
|
66
206
|
var TransitionController = class extends Controller {
|
|
67
207
|
static values = {
|
|
68
208
|
enter: { type: String, default: "" },
|
|
@@ -75,9 +215,9 @@ var TransitionController = class extends Controller {
|
|
|
75
215
|
};
|
|
76
216
|
static actions = ["enter", "leave", "toggle"];
|
|
77
217
|
static events = ["entered", "left"];
|
|
78
|
-
|
|
218
|
+
/** Owns the cancellable completion wait (terminal events + bounded fallback). */
|
|
219
|
+
#transition = new TransitionCompletion();
|
|
79
220
|
#rafId = null;
|
|
80
|
-
#endListener = null;
|
|
81
221
|
connect() {
|
|
82
222
|
this.#strip();
|
|
83
223
|
this.element.setAttribute("data-transition-state", this.element.hidden ? "left" : "entered");
|
|
@@ -104,7 +244,7 @@ var TransitionController = class extends Controller {
|
|
|
104
244
|
const isEnter = kind === "enter";
|
|
105
245
|
if (isEnter) this.element.hidden = false;
|
|
106
246
|
this.element.setAttribute("data-transition-state", isEnter ? "entering" : "leaving");
|
|
107
|
-
if (
|
|
247
|
+
if (prefersReducedMotion()) {
|
|
108
248
|
this.#finish(kind);
|
|
109
249
|
return;
|
|
110
250
|
}
|
|
@@ -116,12 +256,13 @@ var TransitionController = class extends Controller {
|
|
|
116
256
|
this.#rafId = null;
|
|
117
257
|
this.#remove(from);
|
|
118
258
|
this.#add(to);
|
|
119
|
-
this.#
|
|
259
|
+
this.#transition.wait(this.element, () => this.#finish(kind), {
|
|
260
|
+
timeoutMs: this.timeoutValue
|
|
261
|
+
});
|
|
120
262
|
});
|
|
121
263
|
}
|
|
122
264
|
/** Settles the element into the completed state, clearing the stage classes. */
|
|
123
265
|
#finish(kind) {
|
|
124
|
-
this.#cleanupEnd();
|
|
125
266
|
this.#strip();
|
|
126
267
|
if (kind === "enter") {
|
|
127
268
|
this.element.setAttribute("data-transition-state", "entered");
|
|
@@ -132,29 +273,13 @@ var TransitionController = class extends Controller {
|
|
|
132
273
|
this.dispatch("left", { detail: {} });
|
|
133
274
|
}
|
|
134
275
|
}
|
|
135
|
-
/** Resolves on the element's own `transitionend`, with a safety timeout fallback. */
|
|
136
|
-
#awaitEnd(done) {
|
|
137
|
-
this.#endListener = (event) => {
|
|
138
|
-
if (event.target === this.element) done();
|
|
139
|
-
};
|
|
140
|
-
this.element.addEventListener("transitionend", this.#endListener);
|
|
141
|
-
const ms = this.timeoutValue > 0 ? this.timeoutValue : this.#duration();
|
|
142
|
-
this.#timers.set(done, ms);
|
|
143
|
-
}
|
|
144
|
-
#cleanupEnd() {
|
|
145
|
-
if (this.#endListener) {
|
|
146
|
-
this.element.removeEventListener("transitionend", this.#endListener);
|
|
147
|
-
this.#endListener = null;
|
|
148
|
-
}
|
|
149
|
-
this.#timers.clearAll();
|
|
150
|
-
}
|
|
151
276
|
/** Cancels any in-flight transition (interruption / teardown). */
|
|
152
277
|
#cancel() {
|
|
153
278
|
if (this.#rafId !== null) {
|
|
154
279
|
this.#cancelRaf(this.#rafId);
|
|
155
280
|
this.#rafId = null;
|
|
156
281
|
}
|
|
157
|
-
this.#
|
|
282
|
+
this.#transition.cancel();
|
|
158
283
|
this.#strip();
|
|
159
284
|
}
|
|
160
285
|
#add(...lists) {
|
|
@@ -176,13 +301,6 @@ var TransitionController = class extends Controller {
|
|
|
176
301
|
this.leaveToValue
|
|
177
302
|
);
|
|
178
303
|
}
|
|
179
|
-
/** Auto-computed safety duration (transition time + delay, with a small buffer). */
|
|
180
|
-
#duration() {
|
|
181
|
-
if (typeof window.getComputedStyle !== "function") return 0;
|
|
182
|
-
const style = window.getComputedStyle(this.element);
|
|
183
|
-
const total = firstTimeMs(style.transitionDuration) + firstTimeMs(style.transitionDelay);
|
|
184
|
-
return total > 0 ? total + 50 : 0;
|
|
185
|
-
}
|
|
186
304
|
#raf(callback) {
|
|
187
305
|
if (typeof window.requestAnimationFrame === "function") {
|
|
188
306
|
return window.requestAnimationFrame(() => callback());
|
|
@@ -193,9 +311,6 @@ var TransitionController = class extends Controller {
|
|
|
193
311
|
if (typeof window.cancelAnimationFrame === "function") window.cancelAnimationFrame(id);
|
|
194
312
|
else window.clearTimeout(id);
|
|
195
313
|
}
|
|
196
|
-
#prefersReducedMotion() {
|
|
197
|
-
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
198
|
-
}
|
|
199
314
|
};
|
|
200
315
|
|
|
201
316
|
export { TransitionController };
|