stimeo-ui 0.3.0 → 0.4.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 +56 -0
- data/dist/controllers/alert_dialog_controller.js +32 -5
- data/dist/controllers/announcer_controller.js +255 -20
- data/dist/controllers/color_picker_controller.js +46 -0
- data/dist/controllers/command_palette_controller.js +32 -5
- data/dist/controllers/confirm_controller.js +32 -5
- data/dist/controllers/countdown_controller.js +112 -12
- data/dist/controllers/date_range_picker_controller.js +50 -0
- data/dist/controllers/dialog_controller.js +32 -5
- data/dist/controllers/drawer_controller.js +32 -5
- data/dist/controllers/empty_state_controller.js +24 -10
- data/dist/controllers/focus_controller.js +32 -5
- data/dist/controllers/frame_loading_controller.js +177 -15
- data/dist/controllers/local_time_controller.js +100 -6
- data/dist/controllers/meter_controller.js +145 -26
- data/dist/controllers/network_status_controller.js +28 -8
- data/dist/controllers/overflow_menu_controller.js +33 -6
- data/dist/controllers/progress_controller.js +116 -9
- data/dist/controllers/range_slider_controller.js +68 -3
- data/dist/controllers/rating_controller.js +53 -0
- data/dist/controllers/relative_time_controller.js +133 -12
- data/dist/controllers/sidebar_controller.js +37 -8
- data/dist/controllers/skeleton_controller.js +73 -20
- data/dist/controllers/slider_controller.js +17 -2
- data/dist/controllers/spinner_controller.js +228 -27
- data/dist/controllers/step_indicator_controller.js +82 -5
- data/dist/controllers/stick_to_bottom_controller.js +60 -10
- data/dist/index.js +1056 -281
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -2,6 +2,23 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/network_status_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/announce.ts
|
|
6
|
+
function announce(message, options = {}) {
|
|
7
|
+
const text = message.trim();
|
|
8
|
+
if (text.length === 0) return;
|
|
9
|
+
window.dispatchEvent(
|
|
10
|
+
new CustomEvent("stimeo--announcer:announce", {
|
|
11
|
+
detail: { message: text, assertive: options.assertive === true }
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
function fillTemplate(template, values) {
|
|
16
|
+
return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
|
|
17
|
+
const replacement = values[name];
|
|
18
|
+
return replacement === void 0 ? match : String(replacement);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
// src/utils/safe_timeout.ts
|
|
6
23
|
var TimerRegistry = class {
|
|
7
24
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -59,20 +76,17 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
59
76
|
var NetworkStatusController = class extends Controller {
|
|
60
77
|
static targets = ["offline", "online"];
|
|
61
78
|
static values = {
|
|
79
|
+
announceText: { type: String, default: "" },
|
|
80
|
+
announceOnlineText: { type: String, default: "" },
|
|
62
81
|
onlineAutoHide: { type: Number, default: 0 }
|
|
63
82
|
};
|
|
64
83
|
static events = ["change"];
|
|
65
84
|
#timers = new SafeTimeout();
|
|
66
85
|
/** Last known connectivity; guards against duplicate-state re-announcements. */
|
|
67
86
|
#online = true;
|
|
68
|
-
/** Banner text captured from the markup so transitions can re-write it. */
|
|
69
|
-
#offlineMessage = "";
|
|
70
|
-
#onlineMessage = "";
|
|
71
87
|
#handleOnline = () => this.#update(true);
|
|
72
88
|
#handleOffline = () => this.#update(false);
|
|
73
89
|
connect() {
|
|
74
|
-
this.#offlineMessage = this.hasOfflineTarget ? (this.offlineTarget.textContent ?? "").trim() : "";
|
|
75
|
-
this.#onlineMessage = this.hasOnlineTarget ? (this.onlineTarget.textContent ?? "").trim() : "";
|
|
76
90
|
if (this.hasOfflineTarget) this.offlineTarget.hidden = true;
|
|
77
91
|
if (this.hasOnlineTarget) this.onlineTarget.hidden = true;
|
|
78
92
|
this.#online = navigator.onLine;
|
|
@@ -86,7 +100,12 @@ var NetworkStatusController = class extends Controller {
|
|
|
86
100
|
window.removeEventListener("offline", this.#handleOffline);
|
|
87
101
|
this.#timers.clearAll();
|
|
88
102
|
}
|
|
89
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Applies a connectivity transition, guarded against duplicate states.
|
|
105
|
+
*
|
|
106
|
+
* The event goes out last, so a listener reading `data-state` or a banner's
|
|
107
|
+
* visibility sees the state the transition landed on rather than the previous one.
|
|
108
|
+
*/
|
|
90
109
|
#update(online) {
|
|
91
110
|
if (online === this.#online) return;
|
|
92
111
|
this.#online = online;
|
|
@@ -96,6 +115,9 @@ var NetworkStatusController = class extends Controller {
|
|
|
96
115
|
} else {
|
|
97
116
|
this.#showOffline();
|
|
98
117
|
}
|
|
118
|
+
announce(fillTemplate(online ? this.announceOnlineTextValue : this.announceTextValue, {}), {
|
|
119
|
+
assertive: !online
|
|
120
|
+
});
|
|
99
121
|
this.dispatch("change", { detail: { online } });
|
|
100
122
|
}
|
|
101
123
|
/** Shows the offline banner and hides the recovery banner. */
|
|
@@ -103,7 +125,6 @@ var NetworkStatusController = class extends Controller {
|
|
|
103
125
|
this.#timers.clearAll();
|
|
104
126
|
if (this.hasOnlineTarget) this.onlineTarget.hidden = true;
|
|
105
127
|
if (this.hasOfflineTarget) {
|
|
106
|
-
this.offlineTarget.textContent = this.#offlineMessage;
|
|
107
128
|
this.offlineTarget.hidden = false;
|
|
108
129
|
}
|
|
109
130
|
}
|
|
@@ -111,7 +132,6 @@ var NetworkStatusController = class extends Controller {
|
|
|
111
132
|
#showOnline() {
|
|
112
133
|
if (this.hasOfflineTarget) this.offlineTarget.hidden = true;
|
|
113
134
|
if (!this.hasOnlineTarget) return;
|
|
114
|
-
this.onlineTarget.textContent = this.#onlineMessage;
|
|
115
135
|
this.onlineTarget.hidden = false;
|
|
116
136
|
if (this.onlineAutoHideValue > 0) {
|
|
117
137
|
this.#timers.set(() => {
|
|
@@ -2,6 +2,35 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/overflow_menu_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/before_cache_reset.ts
|
|
6
|
+
var BeforeCacheReset = class _BeforeCacheReset {
|
|
7
|
+
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
8
|
+
static #subscribers = /* @__PURE__ */ new Set();
|
|
9
|
+
/** The shared listener; installed while at least one instance is subscribed. */
|
|
10
|
+
static #onBeforeCache = () => {
|
|
11
|
+
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
12
|
+
};
|
|
13
|
+
#rewind;
|
|
14
|
+
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
15
|
+
constructor(rewind) {
|
|
16
|
+
this.#rewind = rewind;
|
|
17
|
+
}
|
|
18
|
+
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
19
|
+
activate() {
|
|
20
|
+
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
21
|
+
_BeforeCacheReset.#subscribers.add(this);
|
|
22
|
+
if (first) {
|
|
23
|
+
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
27
|
+
deactivate() {
|
|
28
|
+
_BeforeCacheReset.#subscribers.delete(this);
|
|
29
|
+
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
30
|
+
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
5
34
|
// src/utils/focus_candidate.ts
|
|
6
35
|
function inheritsFieldsetDisabled(control) {
|
|
7
36
|
let fieldset = control.closest("fieldset[disabled]");
|
|
@@ -183,17 +212,15 @@ var OverflowMenuController = class extends Controller {
|
|
|
183
212
|
#lastHidden = null;
|
|
184
213
|
/** The `tabindex` this instance lends the root for the focus fallback. */
|
|
185
214
|
#tabindex = new TabindexLoan();
|
|
186
|
-
/** Hands Turbo a pristine snapshot
|
|
187
|
-
#
|
|
188
|
-
this.#restoreAll();
|
|
189
|
-
};
|
|
215
|
+
/** Hands Turbo a pristine snapshot of the bar, with every item back in place. */
|
|
216
|
+
#beforeCache = new BeforeCacheReset(() => this.#restoreAll());
|
|
190
217
|
connect() {
|
|
191
218
|
if (!this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
192
219
|
const trigger = this.#trigger();
|
|
193
220
|
if (trigger !== null && this.#isBareTrigger(trigger)) {
|
|
194
221
|
trigger.textContent = this.moreLabelValue;
|
|
195
222
|
}
|
|
196
|
-
|
|
223
|
+
this.#beforeCache.activate();
|
|
197
224
|
this.#layout.observe(this.element);
|
|
198
225
|
this.#layout.observeViewport();
|
|
199
226
|
this.update();
|
|
@@ -201,7 +228,7 @@ var OverflowMenuController = class extends Controller {
|
|
|
201
228
|
disconnect() {
|
|
202
229
|
this.#layout.disconnect();
|
|
203
230
|
this.#timers.clearAll();
|
|
204
|
-
|
|
231
|
+
this.#beforeCache.deactivate();
|
|
205
232
|
this.#restoreAll();
|
|
206
233
|
this.#lastHidden = null;
|
|
207
234
|
}
|
|
@@ -2,6 +2,23 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/progress_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/announce.ts
|
|
6
|
+
function announce(message, options = {}) {
|
|
7
|
+
const text = message.trim();
|
|
8
|
+
if (text.length === 0) return;
|
|
9
|
+
window.dispatchEvent(
|
|
10
|
+
new CustomEvent("stimeo--announcer:announce", {
|
|
11
|
+
detail: { message: text, assertive: options.assertive === true }
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
function fillTemplate(template, values) {
|
|
16
|
+
return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
|
|
17
|
+
const replacement = values[name];
|
|
18
|
+
return replacement === void 0 ? match : String(replacement);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
// src/utils/coerce.ts
|
|
6
23
|
function toFiniteNumber(raw) {
|
|
7
24
|
if (raw === null || raw === void 0 || raw === "") return null;
|
|
@@ -9,7 +26,57 @@ function toFiniteNumber(raw) {
|
|
|
9
26
|
return Number.isFinite(value) ? value : null;
|
|
10
27
|
}
|
|
11
28
|
|
|
29
|
+
// src/utils/microtask_coalescer.ts
|
|
30
|
+
var MicrotaskCoalescer = class {
|
|
31
|
+
#run;
|
|
32
|
+
#queued = false;
|
|
33
|
+
#active = false;
|
|
34
|
+
#generation = 0;
|
|
35
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
36
|
+
constructor(run) {
|
|
37
|
+
this.#run = run;
|
|
38
|
+
}
|
|
39
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
40
|
+
activate() {
|
|
41
|
+
this.#active = true;
|
|
42
|
+
}
|
|
43
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
44
|
+
cancel() {
|
|
45
|
+
this.#active = false;
|
|
46
|
+
this.#queued = false;
|
|
47
|
+
this.#generation += 1;
|
|
48
|
+
}
|
|
49
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
50
|
+
schedule() {
|
|
51
|
+
if (!this.#active || this.#queued) return;
|
|
52
|
+
this.#queued = true;
|
|
53
|
+
const generation = this.#generation;
|
|
54
|
+
queueMicrotask(() => {
|
|
55
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
56
|
+
this.#queued = false;
|
|
57
|
+
this.#run();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/utils/range.ts
|
|
63
|
+
function rangeFraction(value, min, max) {
|
|
64
|
+
const span = max - min;
|
|
65
|
+
if (!(span > 0)) return 0;
|
|
66
|
+
const clamped = Math.min(max, Math.max(min, value));
|
|
67
|
+
let fraction;
|
|
68
|
+
if (Number.isFinite(span)) {
|
|
69
|
+
fraction = (clamped - min) / span;
|
|
70
|
+
} else {
|
|
71
|
+
const scale = Math.max(Math.abs(min), Math.abs(max));
|
|
72
|
+
fraction = (clamped / scale - min / scale) / (max / scale - min / scale);
|
|
73
|
+
}
|
|
74
|
+
if (!Number.isFinite(fraction)) return 0;
|
|
75
|
+
return Math.min(1, Math.max(0, fraction));
|
|
76
|
+
}
|
|
77
|
+
|
|
12
78
|
// src/controllers/progress_controller.ts
|
|
79
|
+
var OWNED_VALUE_TEXT = "data-stimeo--progress-owns-valuetext";
|
|
13
80
|
var ProgressController = class extends Controller {
|
|
14
81
|
static targets = ["bar"];
|
|
15
82
|
static values = {
|
|
@@ -17,13 +84,27 @@ var ProgressController = class extends Controller {
|
|
|
17
84
|
min: { type: Number, default: 0 },
|
|
18
85
|
max: { type: Number, default: 100 },
|
|
19
86
|
indeterminate: { type: Boolean, default: false },
|
|
20
|
-
valueText: { type: String, default: "" }
|
|
87
|
+
valueText: { type: String, default: "" },
|
|
88
|
+
announceText: { type: String, default: "" }
|
|
21
89
|
};
|
|
22
90
|
static actions = ["setValue"];
|
|
23
91
|
static events = ["change", "complete"];
|
|
92
|
+
/**
|
|
93
|
+
* Collapses a morph that swaps several render inputs at once into one repaint.
|
|
94
|
+
* A single update usually rewrites the whole set, and each Value would otherwise
|
|
95
|
+
* repaint on its own.
|
|
96
|
+
*/
|
|
97
|
+
#repaint = new MicrotaskCoalescer(() => {
|
|
98
|
+
this.#render();
|
|
99
|
+
});
|
|
24
100
|
connect() {
|
|
101
|
+
this.#repaint.activate();
|
|
25
102
|
this.#render();
|
|
26
103
|
}
|
|
104
|
+
/** Closes the window in which a queued repaint may still run. */
|
|
105
|
+
disconnect() {
|
|
106
|
+
this.#repaint.cancel();
|
|
107
|
+
}
|
|
27
108
|
/**
|
|
28
109
|
* Updates the progress value from an action param (`amount`) or a
|
|
29
110
|
* `detail.value` CustomEvent, normalizes it into range, syncs ARIA, and
|
|
@@ -39,11 +120,30 @@ var ProgressController = class extends Controller {
|
|
|
39
120
|
this.dispatch("change", { detail: { value, ratio: this.#ratio } });
|
|
40
121
|
if (value >= this.maxValue) {
|
|
41
122
|
this.dispatch("complete", { detail: { value } });
|
|
123
|
+
announce(
|
|
124
|
+
fillTemplate(this.announceTextValue, { value, percent: Math.round(this.#ratio * 100) })
|
|
125
|
+
);
|
|
42
126
|
}
|
|
43
127
|
}
|
|
44
|
-
/**
|
|
128
|
+
/** Repaints when application code (or a Turbo morph) changes `indeterminate` at runtime. */
|
|
45
129
|
indeterminateValueChanged() {
|
|
46
|
-
this.#
|
|
130
|
+
this.#repaint.schedule();
|
|
131
|
+
}
|
|
132
|
+
/** Repaints when application code (or a Turbo morph) changes `value` at runtime. */
|
|
133
|
+
valueValueChanged() {
|
|
134
|
+
this.#repaint.schedule();
|
|
135
|
+
}
|
|
136
|
+
/** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
|
|
137
|
+
minValueChanged() {
|
|
138
|
+
this.#repaint.schedule();
|
|
139
|
+
}
|
|
140
|
+
/** Repaints when application code (or a Turbo morph) changes `max` at runtime. */
|
|
141
|
+
maxValueChanged() {
|
|
142
|
+
this.#repaint.schedule();
|
|
143
|
+
}
|
|
144
|
+
/** Repaints when application code (or a Turbo morph) changes `valueText` at runtime. */
|
|
145
|
+
valueTextValueChanged() {
|
|
146
|
+
this.#repaint.schedule();
|
|
47
147
|
}
|
|
48
148
|
/** Clamps `raw` into the configured `[min, max]` range. */
|
|
49
149
|
#clamp(raw) {
|
|
@@ -51,9 +151,7 @@ var ProgressController = class extends Controller {
|
|
|
51
151
|
}
|
|
52
152
|
/** Current fraction of the range in `[0, 1]`; `0` when the range is empty. */
|
|
53
153
|
get #ratio() {
|
|
54
|
-
|
|
55
|
-
if (span <= 0) return 0;
|
|
56
|
-
return (this.#clamp(this.valueValue) - this.minValue) / span;
|
|
154
|
+
return rangeFraction(this.valueValue, this.minValue, this.maxValue);
|
|
57
155
|
}
|
|
58
156
|
/** Reflects value/range/indeterminate onto ARIA, `data-state`, and the ratio. */
|
|
59
157
|
#render() {
|
|
@@ -61,7 +159,7 @@ var ProgressController = class extends Controller {
|
|
|
61
159
|
this.element.setAttribute("aria-valuemax", String(this.maxValue));
|
|
62
160
|
if (this.indeterminateValue) {
|
|
63
161
|
this.element.removeAttribute("aria-valuenow");
|
|
64
|
-
this
|
|
162
|
+
this.#clearOwnValueText();
|
|
65
163
|
this.element.style.removeProperty("--stimeo-progress-ratio");
|
|
66
164
|
this.element.setAttribute("data-state", "indeterminate");
|
|
67
165
|
return;
|
|
@@ -75,16 +173,25 @@ var ProgressController = class extends Controller {
|
|
|
75
173
|
/**
|
|
76
174
|
* Sets `aria-valuetext` from the consumer-provided template, substituting
|
|
77
175
|
* `{value}` and `{percent}`. Left to the consumer so the human-readable text
|
|
78
|
-
* stays i18n-neutral in the library
|
|
176
|
+
* stays i18n-neutral in the library. With no template the attribute belongs to
|
|
177
|
+
* the consumer, so only a text this controller wrote is taken back
|
|
178
|
+
* ({@link OWNED_VALUE_TEXT}).
|
|
79
179
|
*/
|
|
80
180
|
#applyValueText(value) {
|
|
81
181
|
if (this.valueTextValue.length === 0) {
|
|
82
|
-
this
|
|
182
|
+
this.#clearOwnValueText();
|
|
83
183
|
return;
|
|
84
184
|
}
|
|
85
185
|
const percent = Math.round(this.#ratio * 100);
|
|
86
186
|
const text = this.valueTextValue.replaceAll("{value}", String(value)).replaceAll("{percent}", String(percent));
|
|
87
187
|
this.element.setAttribute("aria-valuetext", text);
|
|
188
|
+
this.element.setAttribute(OWNED_VALUE_TEXT, "");
|
|
189
|
+
}
|
|
190
|
+
/** Removes `aria-valuetext` only when this controller is the one that wrote it. */
|
|
191
|
+
#clearOwnValueText() {
|
|
192
|
+
if (!this.element.hasAttribute(OWNED_VALUE_TEXT)) return;
|
|
193
|
+
this.element.removeAttribute("aria-valuetext");
|
|
194
|
+
this.element.removeAttribute(OWNED_VALUE_TEXT);
|
|
88
195
|
}
|
|
89
196
|
};
|
|
90
197
|
|
|
@@ -18,6 +18,55 @@ 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/range.ts
|
|
55
|
+
function rangeFraction(value, min, max) {
|
|
56
|
+
const span = max - min;
|
|
57
|
+
if (!(span > 0)) return 0;
|
|
58
|
+
const clamped = Math.min(max, Math.max(min, value));
|
|
59
|
+
let fraction;
|
|
60
|
+
if (Number.isFinite(span)) {
|
|
61
|
+
fraction = (clamped - min) / span;
|
|
62
|
+
} else {
|
|
63
|
+
const scale = Math.max(Math.abs(min), Math.abs(max));
|
|
64
|
+
fraction = (clamped / scale - min / scale) / (max / scale - min / scale);
|
|
65
|
+
}
|
|
66
|
+
if (!Number.isFinite(fraction)) return 0;
|
|
67
|
+
return Math.min(1, Math.max(0, fraction));
|
|
68
|
+
}
|
|
69
|
+
|
|
21
70
|
// src/controllers/range_slider_controller.ts
|
|
22
71
|
var START_PROPERTY = "--stimeo-range-start";
|
|
23
72
|
var END_PROPERTY = "--stimeo-range-end";
|
|
@@ -40,16 +89,33 @@ var RangeSliderController = class extends Controller {
|
|
|
40
89
|
return this.logicalTrackValue && isRtl(this.element);
|
|
41
90
|
}
|
|
42
91
|
/** Normalizes the initial pair (clamped, snapped, ordered) and renders. */
|
|
92
|
+
/**
|
|
93
|
+
* Collapses a morph that swaps render inputs into one repaint, and refuses the
|
|
94
|
+
* pass Stimulus delivers before `connect()`.
|
|
95
|
+
*/
|
|
96
|
+
#repaint = new MicrotaskCoalescer(() => {
|
|
97
|
+
this.#render(this.startValue, this.endValue);
|
|
98
|
+
});
|
|
43
99
|
connect() {
|
|
100
|
+
this.#repaint.activate();
|
|
44
101
|
const lo = Math.min(this.startValue, this.endValue);
|
|
45
102
|
const hi = Math.max(this.startValue, this.endValue);
|
|
46
103
|
this.#commit(lo, hi, false);
|
|
47
104
|
}
|
|
48
105
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
49
106
|
disconnect() {
|
|
107
|
+
this.#repaint.cancel();
|
|
50
108
|
this.#dragAbort?.abort();
|
|
51
109
|
this.#dragAbort = null;
|
|
52
110
|
}
|
|
111
|
+
/** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
|
|
112
|
+
minValueChanged() {
|
|
113
|
+
this.#repaint.schedule();
|
|
114
|
+
}
|
|
115
|
+
/** Repaints when application code (or a Turbo morph) changes `max` at runtime. */
|
|
116
|
+
maxValueChanged() {
|
|
117
|
+
this.#repaint.schedule();
|
|
118
|
+
}
|
|
53
119
|
/** Keyboard stepping for whichever thumb is focused (the action's element). */
|
|
54
120
|
onKeydown(event) {
|
|
55
121
|
if (isReservedArrowChord(event)) return;
|
|
@@ -165,14 +231,13 @@ var RangeSliderController = class extends Controller {
|
|
|
165
231
|
this.endThumbTarget.setAttribute("aria-valuemax", String(this.maxValue));
|
|
166
232
|
this.endThumbTarget.setAttribute("aria-valuenow", String(end));
|
|
167
233
|
}
|
|
168
|
-
const span = this.maxValue - this.minValue;
|
|
169
234
|
this.element.style.setProperty(
|
|
170
235
|
START_PROPERTY,
|
|
171
|
-
String(
|
|
236
|
+
String(rangeFraction(start, this.minValue, this.maxValue))
|
|
172
237
|
);
|
|
173
238
|
this.element.style.setProperty(
|
|
174
239
|
END_PROPERTY,
|
|
175
|
-
String(
|
|
240
|
+
String(rangeFraction(end, this.minValue, this.maxValue))
|
|
176
241
|
);
|
|
177
242
|
}
|
|
178
243
|
/** Clamps `raw` to `[min, max]` and snaps it to the nearest step from `min`. */
|
|
@@ -13,6 +13,39 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
13
13
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// src/utils/microtask_coalescer.ts
|
|
17
|
+
var MicrotaskCoalescer = class {
|
|
18
|
+
#run;
|
|
19
|
+
#queued = false;
|
|
20
|
+
#active = false;
|
|
21
|
+
#generation = 0;
|
|
22
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
23
|
+
constructor(run) {
|
|
24
|
+
this.#run = run;
|
|
25
|
+
}
|
|
26
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
27
|
+
activate() {
|
|
28
|
+
this.#active = true;
|
|
29
|
+
}
|
|
30
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
31
|
+
cancel() {
|
|
32
|
+
this.#active = false;
|
|
33
|
+
this.#queued = false;
|
|
34
|
+
this.#generation += 1;
|
|
35
|
+
}
|
|
36
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
37
|
+
schedule() {
|
|
38
|
+
if (!this.#active || this.#queued) return;
|
|
39
|
+
this.#queued = true;
|
|
40
|
+
const generation = this.#generation;
|
|
41
|
+
queueMicrotask(() => {
|
|
42
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
43
|
+
this.#queued = false;
|
|
44
|
+
this.#run();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
16
49
|
// src/utils/roving_tabindex.ts
|
|
17
50
|
var RovingTabindex = class {
|
|
18
51
|
/** Returns the current ordered item elements; called on every operation. */
|
|
@@ -59,13 +92,33 @@ var RatingController = class extends Controller {
|
|
|
59
92
|
static events = ["change"];
|
|
60
93
|
#roving = new RovingTabindex(() => this.symbolTargets);
|
|
61
94
|
/** Reflects the initial value, or switches to the non-interactive readonly view. */
|
|
95
|
+
/**
|
|
96
|
+
* Collapses a morph that swaps render inputs into one repaint, and refuses the
|
|
97
|
+
* pass Stimulus delivers before `connect()`.
|
|
98
|
+
*/
|
|
99
|
+
#repaint = new MicrotaskCoalescer(() => {
|
|
100
|
+
if (this.readonlyValue) {
|
|
101
|
+
this.#applyReadonly();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.#apply(this.#clamp(this.valueValue), { focus: false });
|
|
105
|
+
});
|
|
62
106
|
connect() {
|
|
107
|
+
this.#repaint.activate();
|
|
63
108
|
if (this.readonlyValue) {
|
|
64
109
|
this.#applyReadonly();
|
|
65
110
|
return;
|
|
66
111
|
}
|
|
67
112
|
this.#apply(this.#clamp(this.valueValue), { focus: false });
|
|
68
113
|
}
|
|
114
|
+
/** Closes the window in which a queued repaint may still run. */
|
|
115
|
+
disconnect() {
|
|
116
|
+
this.#repaint.cancel();
|
|
117
|
+
}
|
|
118
|
+
/** Repaints when application code (or a Turbo morph) changes `value` at runtime. */
|
|
119
|
+
valueValueChanged() {
|
|
120
|
+
this.#repaint.schedule();
|
|
121
|
+
}
|
|
69
122
|
/** Selects (or clears) the clicked symbol. Bound via `data-action` (click). */
|
|
70
123
|
select(event) {
|
|
71
124
|
if (this.readonlyValue) return;
|