stimeo-ui 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var ListenerSet = class {
|
|
4
|
+
#abort = new AbortController();
|
|
5
|
+
add(target, type, handler, options) {
|
|
6
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
7
|
+
}
|
|
8
|
+
dispose() {
|
|
9
|
+
this.#abort.abort();
|
|
10
|
+
this.#abort = new AbortController();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
4
13
|
|
|
5
|
-
// src/utils/microtask_coalescer.ts
|
|
6
14
|
var MicrotaskCoalescer = class {
|
|
7
15
|
#run;
|
|
8
16
|
#queued = false;
|
|
9
17
|
#active = false;
|
|
10
18
|
#generation = 0;
|
|
11
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
12
19
|
constructor(run) {
|
|
13
20
|
this.#run = run;
|
|
14
21
|
}
|
|
15
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
16
22
|
activate() {
|
|
17
23
|
this.#active = true;
|
|
18
24
|
}
|
|
19
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
20
25
|
cancel() {
|
|
21
26
|
this.#active = false;
|
|
22
27
|
this.#queued = false;
|
|
23
28
|
this.#generation += 1;
|
|
24
29
|
}
|
|
25
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
26
30
|
schedule() {
|
|
27
31
|
if (!this.#active || this.#queued) return;
|
|
28
32
|
this.#queued = true;
|
|
@@ -35,52 +39,108 @@ var MicrotaskCoalescer = class {
|
|
|
35
39
|
}
|
|
36
40
|
};
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
var MorphRenderWatcher = class {
|
|
43
|
+
#listeners = new ListenerSet();
|
|
44
|
+
#pass;
|
|
45
|
+
#element = null;
|
|
46
|
+
#onMorph = () => this.#pass.schedule();
|
|
47
|
+
constructor(run) {
|
|
48
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
49
|
+
}
|
|
50
|
+
observe(element) {
|
|
51
|
+
if (this.#element === element) return;
|
|
52
|
+
this.disconnect();
|
|
53
|
+
this.#element = element;
|
|
54
|
+
this.#pass.activate();
|
|
55
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
56
|
+
}
|
|
57
|
+
schedule() {
|
|
58
|
+
this.#pass.schedule();
|
|
59
|
+
}
|
|
60
|
+
disconnect() {
|
|
61
|
+
this.#listeners.dispose();
|
|
62
|
+
this.#pass.cancel();
|
|
63
|
+
this.#element = null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
var NUMBER_BOUNDS = {
|
|
68
|
+
finite: { finite: true }};
|
|
69
|
+
function matchesNumberBounds(value, bounds) {
|
|
70
|
+
if (!Number.isFinite(value)) {
|
|
71
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
72
|
+
if (direction === null) return false;
|
|
73
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
74
|
+
}
|
|
75
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
76
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
77
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
78
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
79
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readNumber(raw, fallback, bounds) {
|
|
84
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
var NumberValueReader = class {
|
|
88
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
89
|
+
read(owner, name, raw, fallback, bounds) {
|
|
90
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
91
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
92
|
+
this.#lastRejected.delete(name);
|
|
93
|
+
return resolved;
|
|
94
|
+
}
|
|
95
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
96
|
+
const literal = owner.element.getAttribute(attribute);
|
|
97
|
+
if (literal === null) {
|
|
98
|
+
this.#lastRejected.delete(name);
|
|
99
|
+
return resolved;
|
|
100
|
+
}
|
|
101
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
102
|
+
this.#lastRejected.set(name, literal);
|
|
103
|
+
console.warn(
|
|
104
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return resolved;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
var StepIndicatorController = class _StepIndicatorController extends Controller {
|
|
112
|
+
#numbers = new NumberValueReader();
|
|
40
113
|
static targets = ["step"];
|
|
41
114
|
static values = {
|
|
42
115
|
index: { type: Number, default: 0 }
|
|
43
116
|
};
|
|
117
|
+
static valueConstraints = {
|
|
118
|
+
index: NUMBER_BOUNDS.finite
|
|
119
|
+
};
|
|
44
120
|
static actions = ["setIndex"];
|
|
45
121
|
static events = ["change"];
|
|
46
|
-
|
|
47
|
-
* Collapses a batch of step callbacks — and a morph that swaps `index` with
|
|
48
|
-
* them — into one repaint. Replacing a list of N steps delivers N callbacks, and
|
|
49
|
-
* each one would otherwise rewrite every step's state.
|
|
50
|
-
*/
|
|
51
|
-
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
52
|
-
/** Renders the initial state from the `index` value. */
|
|
122
|
+
#repaint = new MorphRenderWatcher(() => this.#render());
|
|
53
123
|
connect() {
|
|
54
|
-
this.#repaint.
|
|
124
|
+
this.#repaint.observe(this.element);
|
|
55
125
|
this.#render();
|
|
56
126
|
}
|
|
57
|
-
/** Closes the window in which a queued repaint may still run. */
|
|
58
127
|
disconnect() {
|
|
59
|
-
this.#repaint.
|
|
128
|
+
this.#repaint.disconnect();
|
|
60
129
|
}
|
|
61
|
-
/** Syncs a step appended or replaced at runtime (the consumer owns the list). */
|
|
62
130
|
stepTargetConnected() {
|
|
63
131
|
this.#repaint.schedule();
|
|
64
132
|
}
|
|
65
|
-
/** Re-derives the remaining steps when one is removed at runtime. */
|
|
66
133
|
stepTargetDisconnected() {
|
|
67
134
|
this.#repaint.schedule();
|
|
68
135
|
}
|
|
69
|
-
/** Repaints when application code (or a Turbo morph) changes `index` at runtime. */
|
|
70
136
|
indexValueChanged() {
|
|
71
137
|
this.#repaint.schedule();
|
|
72
138
|
}
|
|
73
|
-
/**
|
|
74
|
-
* Moves the indicator from an external event (`detail.index`, 0-based) and
|
|
75
|
-
* dispatches `change`. Out-of-range positions are clamped to the step set,
|
|
76
|
-
* and both sides of the no-op test are clamped, so moving onto the step an
|
|
77
|
-
* out-of-range `index` already renders is not reported as a change.
|
|
78
|
-
*/
|
|
79
139
|
setIndex(event) {
|
|
80
140
|
const next = event.detail?.index;
|
|
81
141
|
if (typeof next !== "number" || !Number.isFinite(next)) return;
|
|
82
142
|
const clamped = this.#clamp(next);
|
|
83
|
-
const previous = this.#clamp(this
|
|
143
|
+
const previous = this.#clamp(this.#safeIndex);
|
|
84
144
|
const moved = clamped !== previous;
|
|
85
145
|
this.indexValue = clamped;
|
|
86
146
|
if (!moved) return;
|
|
@@ -89,18 +149,9 @@ var StepIndicatorController = class extends Controller {
|
|
|
89
149
|
detail: { index: clamped, previous, total: this.stepTargets.length }
|
|
90
150
|
});
|
|
91
151
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Applies `data-state`, `aria-current`, and the progress ratio custom property.
|
|
94
|
-
*
|
|
95
|
-
* A pure function of the step set and `index`, so running it again writes the
|
|
96
|
-
* same values — which is what lets the action path paint synchronously (the event
|
|
97
|
-
* goes out after the DOM is updated) while a coalesced pass may still follow.
|
|
98
|
-
*
|
|
99
|
-
* @stimeoRenderRoot
|
|
100
|
-
*/
|
|
101
152
|
#render() {
|
|
102
153
|
const total = this.stepTargets.length;
|
|
103
|
-
const current = this.#clamp(this
|
|
154
|
+
const current = this.#clamp(this.#safeIndex);
|
|
104
155
|
this.stepTargets.forEach((step, index) => {
|
|
105
156
|
step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
|
|
106
157
|
if (index === current) {
|
|
@@ -112,19 +163,21 @@ var StepIndicatorController = class extends Controller {
|
|
|
112
163
|
const ratio = total > 1 ? current / (total - 1) : 0;
|
|
113
164
|
this.element.style.setProperty("--stimeo--step-indicator-ratio", String(ratio));
|
|
114
165
|
}
|
|
115
|
-
/**
|
|
116
|
-
* Constrains an index to `[0, total-1]` (or `0` when there are no steps). A
|
|
117
|
-
* non-finite index falls back to the first step: `index` is read from markup,
|
|
118
|
-
* so an unparsable attribute arrives as `NaN` and would otherwise propagate
|
|
119
|
-
* into every state hook.
|
|
120
|
-
*/
|
|
121
166
|
#clamp(index) {
|
|
122
167
|
const last = this.stepTargets.length - 1;
|
|
123
|
-
if (last < 0
|
|
168
|
+
if (last < 0) return 0;
|
|
124
169
|
return Math.min(last, Math.max(0, Math.trunc(index)));
|
|
125
170
|
}
|
|
171
|
+
get #safeIndex() {
|
|
172
|
+
return this.#numbers.read(
|
|
173
|
+
this,
|
|
174
|
+
"index",
|
|
175
|
+
this.indexValue,
|
|
176
|
+
_StepIndicatorController.values.index.default,
|
|
177
|
+
_StepIndicatorController.valueConstraints.index
|
|
178
|
+
);
|
|
179
|
+
}
|
|
126
180
|
};
|
|
127
181
|
|
|
128
182
|
export { StepIndicatorController };
|
|
129
|
-
//# sourceMappingURL=step_indicator_controller.js.map
|
|
130
183
|
//# sourceMappingURL=step_indicator_controller.js.map
|
|
@@ -1,28 +1,58 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var NUMBER_BOUNDS = {
|
|
4
|
+
finite: { finite: true }};
|
|
5
|
+
function matchesNumberBounds(value, bounds) {
|
|
6
|
+
if (!Number.isFinite(value)) {
|
|
7
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
8
|
+
if (direction === null) return false;
|
|
9
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
10
|
+
}
|
|
11
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
12
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
13
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
14
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
15
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toFiniteNumber(raw) {
|
|
20
|
+
if (typeof raw !== "number" && typeof raw !== "string") return null;
|
|
21
|
+
if (typeof raw === "string" && raw.trim().length === 0) return null;
|
|
22
|
+
const value = Number(raw);
|
|
23
|
+
return Number.isFinite(value) ? value : null;
|
|
24
|
+
}
|
|
25
|
+
function readNumber(raw, fallback, bounds) {
|
|
26
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var ListenerSet = class {
|
|
30
|
+
#abort = new AbortController();
|
|
31
|
+
add(target, type, handler, options) {
|
|
32
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
33
|
+
}
|
|
34
|
+
dispose() {
|
|
35
|
+
this.#abort.abort();
|
|
36
|
+
this.#abort = new AbortController();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
4
39
|
|
|
5
|
-
// src/utils/microtask_coalescer.ts
|
|
6
40
|
var MicrotaskCoalescer = class {
|
|
7
41
|
#run;
|
|
8
42
|
#queued = false;
|
|
9
43
|
#active = false;
|
|
10
44
|
#generation = 0;
|
|
11
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
12
45
|
constructor(run) {
|
|
13
46
|
this.#run = run;
|
|
14
47
|
}
|
|
15
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
16
48
|
activate() {
|
|
17
49
|
this.#active = true;
|
|
18
50
|
}
|
|
19
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
20
51
|
cancel() {
|
|
21
52
|
this.#active = false;
|
|
22
53
|
this.#queued = false;
|
|
23
54
|
this.#generation += 1;
|
|
24
55
|
}
|
|
25
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
26
56
|
schedule() {
|
|
27
57
|
if (!this.#active || this.#queued) return;
|
|
28
58
|
this.#queued = true;
|
|
@@ -35,95 +65,113 @@ var MicrotaskCoalescer = class {
|
|
|
35
65
|
}
|
|
36
66
|
};
|
|
37
67
|
|
|
38
|
-
|
|
39
|
-
|
|
68
|
+
var MorphRenderWatcher = class {
|
|
69
|
+
#listeners = new ListenerSet();
|
|
70
|
+
#pass;
|
|
71
|
+
#element = null;
|
|
72
|
+
#onMorph = () => this.#pass.schedule();
|
|
73
|
+
constructor(run) {
|
|
74
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
75
|
+
}
|
|
76
|
+
observe(element) {
|
|
77
|
+
if (this.#element === element) return;
|
|
78
|
+
this.disconnect();
|
|
79
|
+
this.#element = element;
|
|
80
|
+
this.#pass.activate();
|
|
81
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
82
|
+
}
|
|
83
|
+
schedule() {
|
|
84
|
+
this.#pass.schedule();
|
|
85
|
+
}
|
|
86
|
+
disconnect() {
|
|
87
|
+
this.#listeners.dispose();
|
|
88
|
+
this.#pass.cancel();
|
|
89
|
+
this.#element = null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
var NumberValueReader = class {
|
|
94
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
95
|
+
read(owner, name, raw, fallback, bounds) {
|
|
96
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
97
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
98
|
+
this.#lastRejected.delete(name);
|
|
99
|
+
return resolved;
|
|
100
|
+
}
|
|
101
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
102
|
+
const literal = owner.element.getAttribute(attribute);
|
|
103
|
+
if (literal === null) {
|
|
104
|
+
this.#lastRejected.delete(name);
|
|
105
|
+
return resolved;
|
|
106
|
+
}
|
|
107
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
108
|
+
this.#lastRejected.set(name, literal);
|
|
109
|
+
console.warn(
|
|
110
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return resolved;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
var StepperController = class _StepperController extends Controller {
|
|
118
|
+
#numbers = new NumberValueReader();
|
|
40
119
|
static targets = ["step"];
|
|
41
120
|
static values = {
|
|
42
121
|
index: { type: Number, default: 0 },
|
|
43
122
|
linear: { type: Boolean, default: false }
|
|
44
123
|
};
|
|
124
|
+
static valueConstraints = {
|
|
125
|
+
index: NUMBER_BOUNDS.finite
|
|
126
|
+
};
|
|
45
127
|
static actions = ["goto", "next", "prev"];
|
|
46
128
|
static events = ["change", "reconcile"];
|
|
47
|
-
|
|
48
|
-
* Collapses the Value and target callbacks of one mutation into one pass, and
|
|
49
|
-
* refuses the ones Stimulus delivers before `connect()`, which renders itself.
|
|
50
|
-
*/
|
|
51
|
-
#repaint = new MicrotaskCoalescer(() => this.#reconcileStep());
|
|
52
|
-
/**
|
|
53
|
-
* The position shown current last, which the next move is measured from, or
|
|
54
|
-
* `null` before any step has been shown. While no step is present it keeps the
|
|
55
|
-
* last one.
|
|
56
|
-
*/
|
|
129
|
+
#repaint = new MorphRenderWatcher(() => this.#reconcileStep());
|
|
57
130
|
#shown = null;
|
|
58
|
-
/** Renders the initial state from `index`, clamped into the step range. */
|
|
59
131
|
connect() {
|
|
60
|
-
this.#repaint.
|
|
132
|
+
this.#repaint.observe(this.element);
|
|
61
133
|
this.#shown = this.#currentStep;
|
|
62
134
|
this.#render();
|
|
63
135
|
}
|
|
64
|
-
/** Drops a pending pass, so nothing renders or reports for a stepper that left. */
|
|
65
136
|
disconnect() {
|
|
66
|
-
this.#repaint.
|
|
137
|
+
this.#repaint.disconnect();
|
|
67
138
|
}
|
|
68
|
-
/** Re-renders when Turbo Morph or application code changes `index` at runtime. */
|
|
69
139
|
indexValueChanged() {
|
|
70
140
|
this.#repaint.schedule();
|
|
71
141
|
}
|
|
72
|
-
/** Re-derives every step's state for a step added at runtime. */
|
|
73
142
|
stepTargetConnected() {
|
|
74
143
|
this.#repaint.schedule();
|
|
75
144
|
}
|
|
76
|
-
/** Re-derives every step's state for a step removed at runtime. */
|
|
77
145
|
stepTargetDisconnected() {
|
|
78
146
|
this.#repaint.schedule();
|
|
79
147
|
}
|
|
80
|
-
/** Advances to the next step (ignored at the last step). */
|
|
81
148
|
next() {
|
|
82
|
-
this.#moveTo(this.#clampIndex(this
|
|
149
|
+
this.#moveTo(this.#clampIndex(this.#safeIndex) + 1);
|
|
83
150
|
}
|
|
84
|
-
/** Returns to the previous step (ignored at the first step). */
|
|
85
151
|
prev() {
|
|
86
|
-
this.#moveTo(this.#clampIndex(this
|
|
152
|
+
this.#moveTo(this.#clampIndex(this.#safeIndex) - 1);
|
|
87
153
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Jumps to the step carried in the action's `index` param. A param that is empty,
|
|
90
|
-
* or that Stimulus parses into anything but a number or a string, is no index,
|
|
91
|
-
* so it jumps nowhere.
|
|
92
|
-
*/
|
|
93
154
|
goto(event) {
|
|
94
155
|
const { index } = event.params;
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
const target = Number(index);
|
|
98
|
-
if (!Number.isFinite(target) || !Number.isInteger(target)) return;
|
|
156
|
+
const target = toFiniteNumber(index);
|
|
157
|
+
if (target === null || !Number.isInteger(target)) return;
|
|
99
158
|
this.#moveTo(target);
|
|
100
159
|
}
|
|
101
|
-
/**
|
|
102
|
-
* Moves the current step to `target` when allowed: in range, not a no-op, and
|
|
103
|
-
* — under `linear` — not skipping more than one step ahead. Re-renders and
|
|
104
|
-
* dispatches `change`.
|
|
105
|
-
*/
|
|
106
160
|
#moveTo(target) {
|
|
107
161
|
const total = this.stepTargets.length;
|
|
108
|
-
if (!Number.isFinite(target) || !Number.isInteger(target)) return;
|
|
109
162
|
if (target < 0 || target >= total) return;
|
|
110
|
-
const current = this.#clampIndex(this
|
|
111
|
-
if (target === current) return;
|
|
163
|
+
const current = this.#clampIndex(this.#safeIndex);
|
|
164
|
+
if (target === current && target === this.#shown) return;
|
|
112
165
|
if (this.linearValue && target > current + 1) return;
|
|
113
|
-
const previous = current;
|
|
166
|
+
const previous = this.#shown ?? current;
|
|
114
167
|
this.indexValue = target;
|
|
115
168
|
this.#shown = target;
|
|
116
169
|
this.#render();
|
|
170
|
+
if (target === previous) return;
|
|
117
171
|
this.dispatch("change", {
|
|
118
172
|
detail: { index: target, previous, total, step: this.stepTargets[target] }
|
|
119
173
|
});
|
|
120
174
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Re-derives one settled batch of Value and target changes, and reports a
|
|
123
|
-
* current position that moved from the one shown before as `reconcile`. With no
|
|
124
|
-
* step present there is nothing to report, and the position shown last stays the
|
|
125
|
-
* one the next move is measured from.
|
|
126
|
-
*/
|
|
127
175
|
#reconcileStep() {
|
|
128
176
|
const previous = this.#shown;
|
|
129
177
|
const index = this.#currentStep;
|
|
@@ -135,25 +183,11 @@ var StepperController = class extends Controller {
|
|
|
135
183
|
detail: { index, previous, total: steps.length, step: steps[index] }
|
|
136
184
|
});
|
|
137
185
|
}
|
|
138
|
-
/** The current position, clamped into the step range, or `null` when there is no step. */
|
|
139
186
|
get #currentStep() {
|
|
140
|
-
return this.stepTargets.length > 0 ? this.#clampIndex(this
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Derives each step's `data-state` and the current button's `aria-current` from
|
|
144
|
-
* `index`, clamped into the step range, without dispatching an action event.
|
|
145
|
-
*
|
|
146
|
-
* The clamp decides only what is shown. This body never writes `index`, so an
|
|
147
|
-
* out-of-range declaration stays in the attribute as the page wrote it.
|
|
148
|
-
*
|
|
149
|
-
* `aria-current="step"` is placed on the step's **first** `<button>`; the markup
|
|
150
|
-
* contract assumes one operable button per step. If a step needs multiple
|
|
151
|
-
* buttons, mark the navigational one first (or this would target the wrong one).
|
|
152
|
-
*
|
|
153
|
-
* @stimeoRenderRoot
|
|
154
|
-
*/
|
|
187
|
+
return this.stepTargets.length > 0 ? this.#clampIndex(this.#safeIndex) : null;
|
|
188
|
+
}
|
|
155
189
|
#render() {
|
|
156
|
-
const current = this.#clampIndex(this
|
|
190
|
+
const current = this.#clampIndex(this.#safeIndex);
|
|
157
191
|
this.stepTargets.forEach((step, index) => {
|
|
158
192
|
step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
|
|
159
193
|
const button = step.querySelector("button");
|
|
@@ -165,14 +199,21 @@ var StepperController = class extends Controller {
|
|
|
165
199
|
}
|
|
166
200
|
});
|
|
167
201
|
}
|
|
168
|
-
/** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */
|
|
169
202
|
#clampIndex(index) {
|
|
170
203
|
const last = this.stepTargets.length - 1;
|
|
171
|
-
if (last < 0
|
|
204
|
+
if (last < 0) return 0;
|
|
172
205
|
return Math.min(last, Math.max(0, Math.trunc(index)));
|
|
173
206
|
}
|
|
207
|
+
get #safeIndex() {
|
|
208
|
+
return this.#numbers.read(
|
|
209
|
+
this,
|
|
210
|
+
"index",
|
|
211
|
+
this.indexValue,
|
|
212
|
+
_StepperController.values.index.default,
|
|
213
|
+
_StepperController.valueConstraints.index
|
|
214
|
+
);
|
|
215
|
+
}
|
|
174
216
|
};
|
|
175
217
|
|
|
176
218
|
export { StepperController };
|
|
177
|
-
//# sourceMappingURL=stepper_controller.js.map
|
|
178
219
|
//# sourceMappingURL=stepper_controller.js.map
|