stimeo-ui 0.15.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 +209 -0
- data/README.md +128 -1
- data/dist/cable/index.js +392 -417
- data/dist/controllers/accordion_controller.js +228 -28
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -191
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +295 -110
- data/dist/controllers/avatar_controller.js +172 -106
- data/dist/controllers/breadcrumb_controller.js +0 -129
- data/dist/controllers/bulk_select_controller.js +264 -53
- data/dist/controllers/calendar_controller.js +346 -180
- data/dist/controllers/carousel_controller.js +587 -300
- data/dist/controllers/character_counter_controller.js +271 -143
- data/dist/controllers/checkbox_controller.js +77 -75
- data/dist/controllers/clipboard_controller.js +187 -108
- data/dist/controllers/collapsible_controller.js +374 -108
- data/dist/controllers/color_picker_controller.js +183 -127
- data/dist/controllers/combobox_controller.js +426 -109
- data/dist/controllers/command_palette_controller.js +994 -350
- data/dist/controllers/conditional_fields_controller.js +212 -125
- data/dist/controllers/confirm_controller.js +939 -245
- data/dist/controllers/context_menu_controller.js +275 -127
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +235 -97
- data/dist/controllers/currency_input_controller.js +241 -118
- data/dist/controllers/data_grid_controller.js +229 -135
- data/dist/controllers/date_range_picker_controller.js +359 -165
- data/dist/controllers/dialog_controller.js +887 -225
- data/dist/controllers/direct_upload_controller.js +80 -142
- data/dist/controllers/dirty_form_controller.js +26 -47
- data/dist/controllers/dismissible_controller.js +175 -20
- data/dist/controllers/drawer_controller.js +963 -336
- data/dist/controllers/dropdown_controller.js +342 -85
- data/dist/controllers/editable_controller.js +182 -58
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +426 -229
- data/dist/controllers/filter_controller.js +78 -36
- data/dist/controllers/flash_controller.js +328 -211
- data/dist/controllers/focus_controller.js +654 -239
- data/dist/controllers/form_field_controller.js +153 -136
- data/dist/controllers/form_validation_controller.js +29 -96
- data/dist/controllers/frame_loading_controller.js +263 -198
- data/dist/controllers/highlight_controller.js +119 -70
- data/dist/controllers/hover_card_controller.js +335 -118
- data/dist/controllers/idle_controller.js +316 -65
- data/dist/controllers/input_mask_controller.js +116 -61
- data/dist/controllers/intersection_controller.js +147 -103
- data/dist/controllers/lazy_frame_controller.js +9 -56
- data/dist/controllers/listbox_controller.js +413 -153
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -95
- data/dist/controllers/menu_controller.js +254 -167
- data/dist/controllers/menubar_controller.js +206 -331
- data/dist/controllers/meter_controller.js +159 -71
- data/dist/controllers/multi_select_controller.js +797 -388
- data/dist/controllers/navigation_menu_controller.js +156 -231
- data/dist/controllers/nested_form_controller.js +373 -174
- data/dist/controllers/network_status_controller.js +241 -45
- data/dist/controllers/number_input_controller.js +387 -227
- data/dist/controllers/optimistic_controller.js +28 -75
- data/dist/controllers/otp_controller.js +344 -196
- data/dist/controllers/overflow_indicator_controller.js +167 -93
- data/dist/controllers/overflow_menu_controller.js +408 -277
- data/dist/controllers/pagination_controller.js +342 -120
- data/dist/controllers/password_reveal_controller.js +424 -99
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +252 -96
- data/dist/controllers/pointer_drag_controller.js +263 -164
- data/dist/controllers/popover_controller.js +267 -94
- data/dist/controllers/portal_controller.js +209 -72
- data/dist/controllers/preview_guard_controller.js +243 -112
- data/dist/controllers/progress_controller.js +132 -59
- data/dist/controllers/radio_group_controller.js +138 -133
- data/dist/controllers/range_slider_controller.js +245 -112
- data/dist/controllers/rating_controller.js +429 -194
- data/dist/controllers/read_more_controller.js +185 -47
- 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 +294 -118
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -221
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -146
- data/dist/controllers/scrollspy_controller.js +181 -236
- data/dist/controllers/separator_controller.js +288 -132
- data/dist/controllers/sidebar_controller.js +1071 -375
- data/dist/controllers/skeleton_controller.js +248 -92
- data/dist/controllers/slider_controller.js +211 -96
- data/dist/controllers/smart_sticky_header_controller.js +163 -54
- data/dist/controllers/sortable_controller.js +13 -110
- data/dist/controllers/spinner_controller.js +277 -161
- data/dist/controllers/step_indicator_controller.js +107 -53
- data/dist/controllers/stepper_controller.js +171 -48
- data/dist/controllers/stick_to_bottom_controller.js +310 -116
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -192
- data/dist/controllers/switch_controller.js +107 -43
- data/dist/controllers/tabs_controller.js +50 -25
- data/dist/controllers/tags_input_controller.js +290 -149
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +148 -80
- data/dist/controllers/time_picker_controller.js +162 -81
- data/dist/controllers/toast_controller.js +365 -193
- data/dist/controllers/toggle_group_controller.js +194 -99
- data/dist/controllers/toolbar_controller.js +92 -90
- data/dist/controllers/tooltip_controller.js +185 -125
- data/dist/controllers/transition_controller.js +123 -115
- data/dist/controllers/tree_view_controller.js +248 -290
- data/dist/index.js +8649 -8572
- data/dist/positioning/index.js +148 -45
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,151 +1,267 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/number_input_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/arrow_step.ts
|
|
6
3
|
function isReservedArrowChord(event, allow = []) {
|
|
7
4
|
if (!event.key.startsWith("Arrow")) return false;
|
|
8
5
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
6
|
}
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
function sharedRegistry(key, create) {
|
|
9
|
+
const symbol = Symbol.for(key);
|
|
10
|
+
const scope = globalThis;
|
|
11
|
+
const existing = scope[symbol];
|
|
12
|
+
if (existing !== void 0) return existing;
|
|
13
|
+
const registry = create();
|
|
14
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
15
|
+
return registry;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isAttributeValue(value) {
|
|
19
|
+
return value === null || typeof value === "string";
|
|
20
|
+
}
|
|
21
|
+
function readLeaseRecord(element, record) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
24
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
25
|
+
const [original, written, ...rest] = parsed;
|
|
26
|
+
const linked = rest[0] !== false;
|
|
27
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
28
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
29
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
30
|
+
} catch {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
var AttributeLease = class _AttributeLease {
|
|
35
|
+
static #registry = sharedRegistry(
|
|
36
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
37
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
38
|
+
);
|
|
13
39
|
#attribute;
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
40
|
+
#record;
|
|
41
|
+
#base;
|
|
42
|
+
#held = /* @__PURE__ */ new Set();
|
|
43
|
+
constructor(attribute, owner, options = {}) {
|
|
17
44
|
this.#attribute = attribute;
|
|
45
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
46
|
+
this.#base = options.base === true;
|
|
47
|
+
const names = _AttributeLease.#registry.names;
|
|
48
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
18
49
|
}
|
|
19
|
-
/** Writes or removes the leased attribute while preserving its authored value. */
|
|
20
50
|
write(element, value) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
this.#reflect(element, value);
|
|
51
|
+
const stack = this.#stack(element);
|
|
52
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
53
|
+
this.#held.add(element);
|
|
54
|
+
const shows = !this.#covered(element, stack, hold);
|
|
55
|
+
hold.written = value;
|
|
56
|
+
this.#mark(element, stack);
|
|
57
|
+
if (shows) this.#reflect(element, value);
|
|
31
58
|
}
|
|
32
|
-
/** Returns one lease without overwriting a value subsequently authored by a consumer. */
|
|
33
59
|
return(element) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
this.#
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
60
|
+
const held = this.#held.delete(element);
|
|
61
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
62
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
63
|
+
const hold = this.#take(stack);
|
|
64
|
+
if (!hold) return false;
|
|
65
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
66
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
67
|
+
const index = stack.indexOf(hold);
|
|
68
|
+
stack.splice(index, 1);
|
|
69
|
+
const above = stack[index];
|
|
70
|
+
if (above) above.linked &&= hold.linked;
|
|
71
|
+
this.#mark(element, stack);
|
|
72
|
+
if (!owns) return true;
|
|
73
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
74
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
returnAll() {
|
|
78
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
79
|
+
}
|
|
80
|
+
#stack(element) {
|
|
81
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
82
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
83
|
+
stacks.set(element, byAttribute);
|
|
84
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
85
|
+
byAttribute.set(this.#attribute, stack);
|
|
86
|
+
return stack;
|
|
87
|
+
}
|
|
88
|
+
#seed(element) {
|
|
89
|
+
const found = [];
|
|
90
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
91
|
+
const read = readLeaseRecord(element, record);
|
|
92
|
+
if (read) found.push({ ...read, record });
|
|
93
|
+
}
|
|
94
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
95
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
96
|
+
lease: null,
|
|
97
|
+
record,
|
|
98
|
+
written,
|
|
99
|
+
original,
|
|
100
|
+
linked: linked && index > 0
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
#take(stack) {
|
|
104
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
105
|
+
if (hold) hold.lease = this;
|
|
106
|
+
return hold;
|
|
107
|
+
}
|
|
108
|
+
#acquire(element, stack) {
|
|
109
|
+
const current = element.getAttribute(this.#attribute);
|
|
110
|
+
const top = stack.at(-1);
|
|
111
|
+
const bottom = stack[0];
|
|
112
|
+
const over = top?.written === current ? top : void 0;
|
|
113
|
+
const from = this.#base ? bottom : over;
|
|
114
|
+
const hold = {
|
|
115
|
+
lease: this,
|
|
116
|
+
record: this.#record,
|
|
117
|
+
written: current,
|
|
118
|
+
original: from ? from.original : current,
|
|
119
|
+
linked: !this.#base && over !== void 0
|
|
120
|
+
};
|
|
121
|
+
if (this.#base && bottom) {
|
|
122
|
+
bottom.linked = true;
|
|
123
|
+
stack.unshift(hold);
|
|
124
|
+
} else stack.push(hold);
|
|
125
|
+
return hold;
|
|
126
|
+
}
|
|
127
|
+
#covered(element, stack, hold) {
|
|
128
|
+
const current = element.getAttribute(this.#attribute);
|
|
129
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
130
|
+
}
|
|
131
|
+
#mark(element, stack) {
|
|
132
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
133
|
+
stack.forEach((hold, index) => {
|
|
134
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
135
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
136
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
137
|
+
records.set(
|
|
138
|
+
hold.record,
|
|
139
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
for (const [name, recorded] of records) {
|
|
143
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
144
|
+
if (recorded === null) element.removeAttribute(name);
|
|
145
|
+
else element.setAttribute(name, recorded);
|
|
146
|
+
}
|
|
39
147
|
}
|
|
40
|
-
/** Reflects only a real value transition, avoiding self-triggered mutation work. */
|
|
41
148
|
#reflect(element, value) {
|
|
42
149
|
if (element.getAttribute(this.#attribute) === value) return;
|
|
43
150
|
if (value === null) element.removeAttribute(this.#attribute);
|
|
44
151
|
else element.setAttribute(this.#attribute, value);
|
|
45
152
|
}
|
|
46
|
-
/** Returns every outstanding lease using the same ownership check as {@link return}. */
|
|
47
|
-
returnAll() {
|
|
48
|
-
for (const element of Array.from(this.#records.keys())) this.return(element);
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// src/utils/before_cache_reset.ts
|
|
53
|
-
var BeforeCacheReset = class _BeforeCacheReset {
|
|
54
|
-
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
55
|
-
static #subscribers = /* @__PURE__ */ new Set();
|
|
56
|
-
/** The shared listener; installed while at least one instance is subscribed. */
|
|
57
|
-
static #onBeforeCache = () => {
|
|
58
|
-
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
59
|
-
};
|
|
60
|
-
#rewind;
|
|
61
|
-
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
62
|
-
constructor(rewind) {
|
|
63
|
-
this.#rewind = rewind;
|
|
64
|
-
}
|
|
65
|
-
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
66
|
-
activate() {
|
|
67
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
68
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
69
|
-
if (first) {
|
|
70
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
74
|
-
deactivate() {
|
|
75
|
-
_BeforeCacheReset.#subscribers.delete(this);
|
|
76
|
-
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
77
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
78
|
-
}
|
|
79
153
|
};
|
|
80
154
|
|
|
81
|
-
|
|
155
|
+
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
156
|
+
"insertCompositionText",
|
|
157
|
+
"insertFromComposition",
|
|
158
|
+
"deleteCompositionText",
|
|
159
|
+
"deleteByComposition"
|
|
160
|
+
]);
|
|
82
161
|
var CompositionTracker = class {
|
|
83
162
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
84
163
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
85
164
|
#onStart;
|
|
86
165
|
#onEnd;
|
|
166
|
+
#confirmedTarget = null;
|
|
87
167
|
constructor(options = {}) {
|
|
88
168
|
this.#onStart = options.onStart;
|
|
89
169
|
this.#onEnd = options.onEnd;
|
|
90
170
|
}
|
|
91
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
92
171
|
observe(target) {
|
|
93
172
|
if (this.#observedTargets.has(target)) return;
|
|
94
173
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
95
174
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
175
|
+
target.addEventListener("keydown", this.#handleKeydown);
|
|
96
176
|
this.#observedTargets.add(target);
|
|
97
177
|
}
|
|
98
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
99
178
|
unobserve(target) {
|
|
100
179
|
if (!this.#observedTargets.delete(target)) return;
|
|
101
180
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
102
181
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
182
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
103
183
|
this.#activeTargets.delete(target);
|
|
184
|
+
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
104
185
|
}
|
|
105
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
106
186
|
disconnect() {
|
|
107
187
|
for (const target of this.#observedTargets) {
|
|
108
188
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
109
189
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
190
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
110
191
|
}
|
|
111
192
|
this.#observedTargets.clear();
|
|
112
193
|
this.#activeTargets.clear();
|
|
194
|
+
this.#confirmedTarget = null;
|
|
113
195
|
}
|
|
114
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
115
196
|
isComposing(event) {
|
|
116
197
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
117
198
|
}
|
|
199
|
+
consumesConfirmedInput(event) {
|
|
200
|
+
const confirmed = this.#confirmedTarget;
|
|
201
|
+
this.#confirmedTarget = null;
|
|
202
|
+
if (confirmed === null || confirmed !== event.target) return false;
|
|
203
|
+
const inputType = event.inputType;
|
|
204
|
+
return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
|
|
205
|
+
}
|
|
118
206
|
#handleStart = (event) => {
|
|
207
|
+
this.#confirmedTarget = null;
|
|
119
208
|
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
120
209
|
this.#onStart?.(event);
|
|
121
210
|
};
|
|
122
211
|
#handleEnd = (event) => {
|
|
123
212
|
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
213
|
+
this.#confirmedTarget = event.target;
|
|
124
214
|
this.#onEnd?.(event);
|
|
125
215
|
};
|
|
216
|
+
#handleKeydown = () => {
|
|
217
|
+
this.#confirmedTarget = null;
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
function commitField(target) {
|
|
222
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
var FULL_WIDTH_SHIFT = 65248;
|
|
226
|
+
function halfWidthChar(char) {
|
|
227
|
+
if (char >= "\uFF01" && char <= "\uFF5E") {
|
|
228
|
+
return String.fromCharCode(char.charCodeAt(0) - FULL_WIDTH_SHIFT);
|
|
229
|
+
}
|
|
230
|
+
return char === "\u3000" ? " " : char;
|
|
231
|
+
}
|
|
232
|
+
function toHalfWidth(text) {
|
|
233
|
+
let out = "";
|
|
234
|
+
for (const char of text) out += halfWidthChar(char);
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
var ListenerSet = class {
|
|
239
|
+
#abort = new AbortController();
|
|
240
|
+
add(target, type, handler, options) {
|
|
241
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
242
|
+
}
|
|
243
|
+
dispose() {
|
|
244
|
+
this.#abort.abort();
|
|
245
|
+
this.#abort = new AbortController();
|
|
246
|
+
}
|
|
126
247
|
};
|
|
127
248
|
|
|
128
|
-
// src/utils/microtask_coalescer.ts
|
|
129
249
|
var MicrotaskCoalescer = class {
|
|
130
250
|
#run;
|
|
131
251
|
#queued = false;
|
|
132
252
|
#active = false;
|
|
133
253
|
#generation = 0;
|
|
134
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
135
254
|
constructor(run) {
|
|
136
255
|
this.#run = run;
|
|
137
256
|
}
|
|
138
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
139
257
|
activate() {
|
|
140
258
|
this.#active = true;
|
|
141
259
|
}
|
|
142
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
143
260
|
cancel() {
|
|
144
261
|
this.#active = false;
|
|
145
262
|
this.#queued = false;
|
|
146
263
|
this.#generation += 1;
|
|
147
264
|
}
|
|
148
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
149
265
|
schedule() {
|
|
150
266
|
if (!this.#active || this.#queued) return;
|
|
151
267
|
this.#queued = true;
|
|
@@ -158,43 +274,107 @@ var MicrotaskCoalescer = class {
|
|
|
158
274
|
}
|
|
159
275
|
};
|
|
160
276
|
|
|
161
|
-
|
|
277
|
+
var MorphRenderWatcher = class {
|
|
278
|
+
#listeners = new ListenerSet();
|
|
279
|
+
#pass;
|
|
280
|
+
#element = null;
|
|
281
|
+
#onMorph = () => this.#pass.schedule();
|
|
282
|
+
constructor(run) {
|
|
283
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
284
|
+
}
|
|
285
|
+
observe(element) {
|
|
286
|
+
if (this.#element === element) return;
|
|
287
|
+
this.disconnect();
|
|
288
|
+
this.#element = element;
|
|
289
|
+
this.#pass.activate();
|
|
290
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
291
|
+
}
|
|
292
|
+
schedule() {
|
|
293
|
+
this.#pass.schedule();
|
|
294
|
+
}
|
|
295
|
+
disconnect() {
|
|
296
|
+
this.#listeners.dispose();
|
|
297
|
+
this.#pass.cancel();
|
|
298
|
+
this.#element = null;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
var MoveCounter = class {
|
|
303
|
+
#sequence = 0;
|
|
304
|
+
record() {
|
|
305
|
+
this.#sequence += 1;
|
|
306
|
+
return this.#sequence;
|
|
307
|
+
}
|
|
308
|
+
isLatest(token) {
|
|
309
|
+
return token === this.#sequence;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
var NUMBER_BOUNDS = {
|
|
314
|
+
nonNegative: { finite: true, min: 0 },
|
|
315
|
+
positive: { finite: true, exclusiveMin: 0 },
|
|
316
|
+
lowerBound: { finite: true, allowInfinity: "negative" },
|
|
317
|
+
upperBound: { finite: true, allowInfinity: "positive" }};
|
|
318
|
+
function matchesNumberBounds(value, bounds) {
|
|
319
|
+
if (!Number.isFinite(value)) {
|
|
320
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
321
|
+
if (direction === null) return false;
|
|
322
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
323
|
+
}
|
|
324
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
325
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
326
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
327
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
328
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function readNumber(raw, fallback, bounds) {
|
|
333
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
var NumberValueReader = class {
|
|
337
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
338
|
+
read(owner, name, raw, fallback, bounds) {
|
|
339
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
340
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
341
|
+
this.#lastRejected.delete(name);
|
|
342
|
+
return resolved;
|
|
343
|
+
}
|
|
344
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
345
|
+
const literal = owner.element.getAttribute(attribute);
|
|
346
|
+
if (literal === null) {
|
|
347
|
+
this.#lastRejected.delete(name);
|
|
348
|
+
return resolved;
|
|
349
|
+
}
|
|
350
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
351
|
+
this.#lastRejected.set(name, literal);
|
|
352
|
+
console.warn(
|
|
353
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return resolved;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
162
360
|
var TimerRegistry = class {
|
|
163
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
164
361
|
ids = /* @__PURE__ */ new Set();
|
|
165
|
-
/**
|
|
166
|
-
* Cancels a single tracked timer.
|
|
167
|
-
*
|
|
168
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
169
|
-
* registry), so callers can clear defensively without guarding.
|
|
170
|
-
*/
|
|
171
362
|
clear(id) {
|
|
172
363
|
if (this.ids.delete(id)) {
|
|
173
364
|
this.cancel(id);
|
|
174
365
|
}
|
|
175
366
|
}
|
|
176
|
-
/**
|
|
177
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
178
|
-
* to guarantee no timer outlives the element.
|
|
179
|
-
*/
|
|
180
367
|
clearAll() {
|
|
181
368
|
for (const id of this.ids) {
|
|
182
369
|
this.cancel(id);
|
|
183
370
|
}
|
|
184
371
|
this.ids.clear();
|
|
185
372
|
}
|
|
186
|
-
/** Number of timers currently tracked (pending). */
|
|
187
373
|
get size() {
|
|
188
374
|
return this.ids.size;
|
|
189
375
|
}
|
|
190
376
|
};
|
|
191
377
|
var SafeTimeout = class extends TimerRegistry {
|
|
192
|
-
/**
|
|
193
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
194
|
-
*
|
|
195
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
196
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
197
|
-
*/
|
|
198
378
|
set(callback, delay) {
|
|
199
379
|
const id = this.schedule(() => {
|
|
200
380
|
this.ids.delete(id);
|
|
@@ -211,7 +391,6 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
211
391
|
}
|
|
212
392
|
};
|
|
213
393
|
var SafeInterval = class extends TimerRegistry {
|
|
214
|
-
/** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
|
|
215
394
|
set(callback, delay) {
|
|
216
395
|
const id = this.schedule(callback, delay);
|
|
217
396
|
this.ids.add(id);
|
|
@@ -225,7 +404,6 @@ var SafeInterval = class extends TimerRegistry {
|
|
|
225
404
|
}
|
|
226
405
|
};
|
|
227
406
|
|
|
228
|
-
// src/utils/stepped_value.ts
|
|
229
407
|
function effectiveStep(step) {
|
|
230
408
|
return Number.isFinite(step) && step > 0 ? step : 1;
|
|
231
409
|
}
|
|
@@ -331,9 +509,11 @@ function finiteFallback(min, max) {
|
|
|
331
509
|
return 0;
|
|
332
510
|
}
|
|
333
511
|
|
|
334
|
-
// src/controllers/number_input_controller.ts
|
|
335
512
|
var OWNED_DISABLED = "data-number-input-disabled";
|
|
336
513
|
var NumberInputController = class _NumberInputController extends Controller {
|
|
514
|
+
#numbers = new NumberValueReader();
|
|
515
|
+
#moves = new MoveCounter();
|
|
516
|
+
#move = 0;
|
|
337
517
|
static targets = ["input", "increment", "decrement"];
|
|
338
518
|
static values = {
|
|
339
519
|
min: { type: Number, default: Number.NEGATIVE_INFINITY },
|
|
@@ -341,51 +521,41 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
341
521
|
step: { type: Number, default: 1 },
|
|
342
522
|
pageStep: { type: Number, default: 0 }
|
|
343
523
|
};
|
|
524
|
+
static valueConstraints = {
|
|
525
|
+
min: NUMBER_BOUNDS.lowerBound,
|
|
526
|
+
max: NUMBER_BOUNDS.upperBound,
|
|
527
|
+
step: NUMBER_BOUNDS.positive,
|
|
528
|
+
pageStep: NUMBER_BOUNDS.nonNegative
|
|
529
|
+
};
|
|
344
530
|
static actions = ["decrement", "increment", "onInput", "onKeydown"];
|
|
345
531
|
static events = ["change", "reconcile"];
|
|
346
|
-
/** Delay (ms) a button must be held before auto-repeat starts. */
|
|
347
532
|
static #HOLD_DELAY_MS = 400;
|
|
348
|
-
/** Interval (ms) between auto-repeat steps once a hold has started. */
|
|
349
533
|
static #HOLD_REPEAT_MS = 80;
|
|
350
|
-
/**
|
|
351
|
-
* Window (ms) after a hold ends during which the trailing synthetic `click` is
|
|
352
|
-
* swallowed. A safety net for the (rare) case where that click never arrives
|
|
353
|
-
* (e.g. the pointer was released off the button): the flag self-clears instead
|
|
354
|
-
* of poisoning the next legitimate click.
|
|
355
|
-
*/
|
|
356
534
|
static #SUPPRESS_RESET_MS = 250;
|
|
357
|
-
/** Aborts global pointer guards on disconnect so none outlive the element. */
|
|
358
535
|
#globalGuards = null;
|
|
359
|
-
/** Per-target guards allow an old button to be released without touching its replacement. */
|
|
360
536
|
#buttonGuards = /* @__PURE__ */ new Map();
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
#
|
|
367
|
-
#
|
|
368
|
-
|
|
537
|
+
#composition = new CompositionTracker({
|
|
538
|
+
onEnd: () => {
|
|
539
|
+
if (this.#reconcileHeld) this.#repaint.schedule();
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
#reconcileHeld = false;
|
|
543
|
+
#ariaValueNow = new AttributeLease("aria-valuenow", this.identifier);
|
|
544
|
+
#ariaValueMin = new AttributeLease("aria-valuemin", this.identifier);
|
|
545
|
+
#ariaValueMax = new AttributeLease("aria-valuemax", this.identifier);
|
|
369
546
|
#lastValue = null;
|
|
370
|
-
|
|
547
|
+
#connected = false;
|
|
371
548
|
#holdTimeouts = new SafeTimeout();
|
|
372
|
-
/** The running auto-repeat interval (one at a time). */
|
|
373
549
|
#holdIntervals = new SafeInterval();
|
|
374
|
-
/** True while a hold is armed/running, making `#stopHold` idempotent. */
|
|
375
550
|
#holdActive = false;
|
|
376
|
-
|
|
551
|
+
#holdGeneration = 0;
|
|
377
552
|
#repeatedDuringHold = false;
|
|
378
|
-
/** True when the next `click` is the trailing one after a hold and must be ignored. */
|
|
379
553
|
#suppressNextClick = false;
|
|
380
|
-
/** Button and pointer that own the current hold, so other instances/pointers cannot end it. */
|
|
381
554
|
#holdButton = null;
|
|
382
555
|
#holdPointerId = null;
|
|
383
|
-
|
|
384
|
-
#repaint = new MicrotaskCoalescer(() => this.#reconcile());
|
|
385
|
-
/** Normalizes any initial value and wires the focus/hold pointer guards. */
|
|
556
|
+
#repaint = new MorphRenderWatcher(() => this.#reconcile());
|
|
386
557
|
connect() {
|
|
387
|
-
this.#repaint.
|
|
388
|
-
this.#beforeCache.activate();
|
|
558
|
+
this.#repaint.observe(this.element);
|
|
389
559
|
this.#globalGuards = new AbortController();
|
|
390
560
|
const { signal } = this.#globalGuards;
|
|
391
561
|
for (const button of this.incrementTargets) this.#wireButton(button, 1);
|
|
@@ -394,12 +564,12 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
394
564
|
window.addEventListener("pointercancel", this.#onPointerEnd, { signal });
|
|
395
565
|
window.addEventListener("blur", this.#onWindowBlur, { signal });
|
|
396
566
|
this.#reconcile();
|
|
567
|
+
this.#connected = true;
|
|
397
568
|
}
|
|
398
|
-
/** Releases listeners, derived attributes, timers, and transient value state. */
|
|
399
569
|
disconnect() {
|
|
400
|
-
this.#
|
|
570
|
+
this.#connected = false;
|
|
571
|
+
this.#repaint.disconnect();
|
|
401
572
|
this.#composition.disconnect();
|
|
402
|
-
this.#beforeCache.deactivate();
|
|
403
573
|
this.#globalGuards?.abort();
|
|
404
574
|
this.#globalGuards = null;
|
|
405
575
|
const buttons = /* @__PURE__ */ new Set([
|
|
@@ -414,72 +584,55 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
414
584
|
this.#repeatedDuringHold = false;
|
|
415
585
|
this.#suppressNextClick = false;
|
|
416
586
|
this.#holdTimeouts.clearAll();
|
|
417
|
-
this.#holdIntervals.clearAll();
|
|
418
587
|
this.#ariaValueNow.returnAll();
|
|
419
588
|
this.#ariaValueMin.returnAll();
|
|
420
589
|
this.#ariaValueMax.returnAll();
|
|
421
|
-
this.#lastValue = null;
|
|
422
590
|
}
|
|
423
|
-
/** Reconciles the value against a minimum changed by application code or a Turbo morph. */
|
|
424
591
|
minValueChanged() {
|
|
425
592
|
this.#repaint.schedule();
|
|
426
593
|
}
|
|
427
|
-
/** Reconciles the value against a maximum changed by application code or a Turbo morph. */
|
|
428
594
|
maxValueChanged() {
|
|
429
595
|
this.#repaint.schedule();
|
|
430
596
|
}
|
|
431
|
-
/** Reconciles the value against a step changed by application code or a Turbo morph. */
|
|
432
597
|
stepValueChanged() {
|
|
433
598
|
this.#repaint.schedule();
|
|
434
599
|
}
|
|
435
|
-
/** Tracks and reconciles an input inserted or replaced at runtime. */
|
|
436
600
|
inputTargetConnected(input) {
|
|
437
601
|
this.#composition.observe(input);
|
|
438
602
|
this.#repaint.schedule();
|
|
439
603
|
}
|
|
440
|
-
/** Releases ARIA and composition state owned by an input that left the controller. */
|
|
441
604
|
inputTargetDisconnected(input) {
|
|
442
605
|
this.#composition.unobserve(input);
|
|
443
606
|
this.#releaseInputAria(input);
|
|
444
607
|
this.#stopHold(false);
|
|
445
|
-
this.#lastValue = null;
|
|
446
608
|
this.#repaint.schedule();
|
|
447
609
|
}
|
|
448
|
-
/** Wires focus preservation and hold behavior on a runtime increment target. */
|
|
449
610
|
incrementTargetConnected(button) {
|
|
450
611
|
this.#wireButton(button, 1);
|
|
451
612
|
this.#repaint.schedule();
|
|
452
613
|
}
|
|
453
|
-
/** Releases only the increment target that actually disconnected. */
|
|
454
614
|
incrementTargetDisconnected(button) {
|
|
455
615
|
this.#unwireButton(button);
|
|
456
|
-
this.#repaint.schedule();
|
|
457
616
|
}
|
|
458
|
-
/** Wires focus preservation and hold behavior on a runtime decrement target. */
|
|
459
617
|
decrementTargetConnected(button) {
|
|
460
618
|
this.#wireButton(button, -1);
|
|
461
619
|
this.#repaint.schedule();
|
|
462
620
|
}
|
|
463
|
-
/** Releases only the decrement target that actually disconnected. */
|
|
464
621
|
decrementTargetDisconnected(button) {
|
|
465
622
|
this.#unwireButton(button);
|
|
466
|
-
this.#repaint.schedule();
|
|
467
623
|
}
|
|
468
|
-
/** Increases by one step. Bound via `data-action` (click). */
|
|
469
624
|
increment() {
|
|
470
625
|
if (this.#consumeSuppressedClick()) return;
|
|
471
626
|
if (!this.hasInputTarget) return;
|
|
472
627
|
this.#commitStep(1);
|
|
473
628
|
this.inputTarget.focus();
|
|
474
629
|
}
|
|
475
|
-
/** Decreases by one step. Bound via `data-action` (click). */
|
|
476
630
|
decrement() {
|
|
477
631
|
if (this.#consumeSuppressedClick()) return;
|
|
478
632
|
if (!this.hasInputTarget) return;
|
|
479
633
|
this.#commitStep(-1);
|
|
480
634
|
this.inputTarget.focus();
|
|
481
635
|
}
|
|
482
|
-
/** Clamps and snaps a typed value. Bound via `data-action` (change). */
|
|
483
636
|
onInput() {
|
|
484
637
|
if (!this.hasInputTarget) return;
|
|
485
638
|
if (this.inputTarget.value.trim() === "") {
|
|
@@ -489,7 +642,6 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
489
642
|
}
|
|
490
643
|
this.#commit(this.#currentValue());
|
|
491
644
|
}
|
|
492
|
-
/** Keyboard stepping per the APG spinbutton model. */
|
|
493
645
|
onKeydown(event) {
|
|
494
646
|
if (!this.hasInputTarget) return;
|
|
495
647
|
if (this.#composition.isComposing(event)) return;
|
|
@@ -503,30 +655,25 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
503
655
|
next = stepSteppedValue(this.#currentValue(), -1, this.#steppedRange);
|
|
504
656
|
break;
|
|
505
657
|
case "PageUp":
|
|
506
|
-
next = this
|
|
658
|
+
next = this.#safePageStep > 0 ? this.#currentValue() + this.#safePageStep : stepSteppedValue(this.#currentValue(), 10, this.#steppedRange);
|
|
507
659
|
break;
|
|
508
660
|
case "PageDown":
|
|
509
|
-
next = this
|
|
661
|
+
next = this.#safePageStep > 0 ? this.#currentValue() - this.#safePageStep : stepSteppedValue(this.#currentValue(), -10, this.#steppedRange);
|
|
510
662
|
break;
|
|
511
663
|
case "Home":
|
|
512
|
-
if (!Number.isFinite(this
|
|
513
|
-
next = this
|
|
664
|
+
if (!Number.isFinite(this.#safeMin)) return;
|
|
665
|
+
next = this.#safeMin;
|
|
514
666
|
break;
|
|
515
667
|
case "End":
|
|
516
|
-
if (!Number.isFinite(this
|
|
517
|
-
next = this
|
|
668
|
+
if (!Number.isFinite(this.#safeMax)) return;
|
|
669
|
+
next = this.#safeMax;
|
|
518
670
|
break;
|
|
519
671
|
default:
|
|
520
672
|
return;
|
|
521
673
|
}
|
|
522
674
|
event.preventDefault();
|
|
523
|
-
this.#commit(next);
|
|
675
|
+
this.#commit(next, true);
|
|
524
676
|
}
|
|
525
|
-
/**
|
|
526
|
-
* Wires a step button: `pointerdown` keeps focus on the input and arms the
|
|
527
|
-
* hold; leaving the button while held stops it (the global listeners cover
|
|
528
|
-
* release/cancel/blur).
|
|
529
|
-
*/
|
|
530
677
|
#wireButton(button, direction) {
|
|
531
678
|
if (this.#buttonGuards.has(button)) return;
|
|
532
679
|
const guard = new AbortController();
|
|
@@ -542,14 +689,12 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
542
689
|
{ signal: guard.signal }
|
|
543
690
|
);
|
|
544
691
|
}
|
|
545
|
-
/** Aborts one target's listeners and returns any state derived onto that button. */
|
|
546
692
|
#unwireButton(button) {
|
|
547
693
|
this.#buttonGuards.get(button)?.abort();
|
|
548
694
|
this.#buttonGuards.delete(button);
|
|
549
695
|
if (this.#holdButton === button) this.#stopHold(false);
|
|
550
696
|
this.#releaseButtonState(button);
|
|
551
697
|
}
|
|
552
|
-
/** Starts a hold: focus the input, then schedule the first repeat after a delay. */
|
|
553
698
|
#armHold(event, button, direction) {
|
|
554
699
|
const pointerButton = event.button;
|
|
555
700
|
if (typeof pointerButton === "number" && pointerButton !== 0) return;
|
|
@@ -562,25 +707,19 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
562
707
|
this.#holdPointerId = this.#pointerId(event);
|
|
563
708
|
this.#repeatedDuringHold = false;
|
|
564
709
|
this.#suppressNextClick = false;
|
|
710
|
+
const generation = ++this.#holdGeneration;
|
|
565
711
|
this.#holdTimeouts.set(() => {
|
|
566
|
-
if (!this.#commitStep(direction))
|
|
567
|
-
this.#stopHold();
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
712
|
+
if (!this.#commitStep(direction) || generation !== this.#holdGeneration) return;
|
|
570
713
|
this.#repeatedDuringHold = true;
|
|
571
714
|
this.#holdIntervals.set(() => {
|
|
572
715
|
if (!this.#commitStep(direction)) this.#stopHold();
|
|
573
716
|
}, _NumberInputController.#HOLD_REPEAT_MS);
|
|
574
717
|
}, _NumberInputController.#HOLD_DELAY_MS);
|
|
575
718
|
}
|
|
576
|
-
/**
|
|
577
|
-
* Stops the current hold (idempotent). When the hold actually repeated, the
|
|
578
|
-
* trailing synthetic `click` must be ignored, so it is suppressed until the
|
|
579
|
-
* click consumes it or a short safety-net timeout clears it.
|
|
580
|
-
*/
|
|
581
719
|
#stopHold(suppressTrailingClick = true) {
|
|
582
720
|
if (!this.#holdActive) return;
|
|
583
721
|
this.#holdActive = false;
|
|
722
|
+
this.#holdGeneration += 1;
|
|
584
723
|
this.#holdButton = null;
|
|
585
724
|
this.#holdPointerId = null;
|
|
586
725
|
this.#holdTimeouts.clearAll();
|
|
@@ -590,117 +729,111 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
590
729
|
this.#holdTimeouts.set(() => {
|
|
591
730
|
this.#suppressNextClick = false;
|
|
592
731
|
}, _NumberInputController.#SUPPRESS_RESET_MS);
|
|
593
|
-
} else if (!suppressTrailingClick) {
|
|
594
|
-
this.#suppressNextClick = false;
|
|
595
732
|
}
|
|
596
733
|
}
|
|
597
|
-
/** Ends a hold only for its initiating pointer (legacy/synthetic events remain accepted). */
|
|
598
734
|
#onPointerEnd = (event) => {
|
|
599
735
|
if (this.#ownsPointer(event)) this.#stopHold();
|
|
600
736
|
};
|
|
601
|
-
/** Window deactivation is global and always ends the active hold. */
|
|
602
737
|
#onWindowBlur = () => {
|
|
603
738
|
this.#stopHold();
|
|
604
739
|
};
|
|
605
|
-
/** Extracts pointer identity when the runtime event exposes it. */
|
|
606
740
|
#pointerId(event) {
|
|
607
741
|
return event.pointerId ?? null;
|
|
608
742
|
}
|
|
609
|
-
/** Whether `event` belongs to the active pointer, with a synthetic-event fallback. */
|
|
610
743
|
#ownsPointer(event) {
|
|
611
744
|
const pointerId = this.#pointerId(event);
|
|
612
745
|
return this.#holdActive && (this.#holdPointerId === null || pointerId === null || pointerId === this.#holdPointerId);
|
|
613
746
|
}
|
|
614
|
-
/** Consumes a pending trailing-click suppression; returns true if the click was swallowed. */
|
|
615
747
|
#consumeSuppressedClick() {
|
|
616
748
|
if (!this.#suppressNextClick) return false;
|
|
617
749
|
this.#suppressNextClick = false;
|
|
618
750
|
this.#holdTimeouts.clearAll();
|
|
619
751
|
return true;
|
|
620
752
|
}
|
|
621
|
-
|
|
622
|
-
* Normalizes `raw`, reflects it, and dispatches `{ value: number }` only when
|
|
623
|
-
* the committed numeric value differs from the last reconciled value.
|
|
624
|
-
*
|
|
625
|
-
* @returns Whether the value changed (drives the auto-repeat's bound stop).
|
|
626
|
-
*/
|
|
627
|
-
#commit(raw) {
|
|
753
|
+
#commit(raw, report = false) {
|
|
628
754
|
const value = this.#normalize(raw);
|
|
629
755
|
const changed = this.#lastValue === null || value !== this.#lastValue;
|
|
630
|
-
this
|
|
756
|
+
const shown = this.inputTarget.value;
|
|
631
757
|
this.#lastValue = value;
|
|
758
|
+
if (changed) this.#move = this.#moves.record();
|
|
759
|
+
const move = this.#move;
|
|
760
|
+
this.#write(value);
|
|
761
|
+
if (!this.#moves.isLatest(move)) return changed;
|
|
762
|
+
if (report && shown !== this.inputTarget.value) {
|
|
763
|
+
const input = this.inputTarget;
|
|
764
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
765
|
+
if (!this.#moves.isLatest(move)) return changed;
|
|
766
|
+
commitField(input);
|
|
767
|
+
}
|
|
768
|
+
if (!this.#moves.isLatest(move)) return changed;
|
|
632
769
|
if (changed) this.dispatch("change", { detail: { value } });
|
|
633
770
|
return changed;
|
|
634
771
|
}
|
|
635
|
-
/** Commits an adjacent endpoint/grid value and reports whether it moved. */
|
|
636
772
|
#commitStep(count) {
|
|
637
773
|
if (!this.hasInputTarget) return false;
|
|
638
|
-
return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange));
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
|
-
* Reflects the current value after a range or step morph, reporting a moved
|
|
642
|
-
* value as `reconcile`.
|
|
643
|
-
*
|
|
644
|
-
* @stimeoRenderRoot
|
|
645
|
-
*/
|
|
774
|
+
return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange), true);
|
|
775
|
+
}
|
|
646
776
|
#reconcile() {
|
|
647
|
-
if (!this.hasInputTarget)
|
|
648
|
-
|
|
777
|
+
if (!this.hasInputTarget) return;
|
|
778
|
+
if (this.#composition.isComposing()) {
|
|
779
|
+
this.#reconcileHeld = true;
|
|
649
780
|
return;
|
|
650
781
|
}
|
|
782
|
+
this.#reconcileHeld = false;
|
|
783
|
+
const previous = this.#lastValue;
|
|
651
784
|
if (this.inputTarget.value.trim() !== "") {
|
|
652
|
-
const previous = this.#lastValue;
|
|
653
785
|
const value = this.#normalize(this.#currentValue());
|
|
654
|
-
this.#write(value);
|
|
655
786
|
this.#lastValue = value;
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
787
|
+
const move = this.#move;
|
|
788
|
+
this.#write(value);
|
|
789
|
+
if (!this.#moves.isLatest(move)) return;
|
|
659
790
|
} else {
|
|
660
791
|
this.#lastValue = null;
|
|
661
792
|
this.#reflectEmpty();
|
|
662
793
|
}
|
|
794
|
+
this.#reportMove(previous);
|
|
795
|
+
}
|
|
796
|
+
#reportMove(previous) {
|
|
797
|
+
const value = this.#lastValue;
|
|
798
|
+
if (!this.#connected || value === null || value === previous) return;
|
|
799
|
+
this.dispatch("reconcile", { detail: { value } });
|
|
663
800
|
}
|
|
664
|
-
/** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */
|
|
665
801
|
#write(value) {
|
|
666
802
|
const input = this.inputTarget;
|
|
667
|
-
input.value = String(value);
|
|
803
|
+
if (input.value !== String(value)) input.value = String(value);
|
|
668
804
|
this.#syncInputAria(input, value);
|
|
669
805
|
this.#updateButtons(value);
|
|
670
806
|
}
|
|
671
|
-
/** Reflects a blank value without inventing a numeric `aria-valuenow`. */
|
|
672
807
|
#reflectEmpty() {
|
|
673
808
|
this.#syncInputAria(this.inputTarget, null);
|
|
674
809
|
this.#updateButtons(this.#currentValue());
|
|
675
810
|
}
|
|
676
|
-
/** Synchronizes controller-derived ARIA while preserving authored teardown values. */
|
|
677
811
|
#syncInputAria(input, value) {
|
|
678
812
|
if (input.getAttribute("role") !== "spinbutton") {
|
|
679
813
|
this.#releaseInputAria(input);
|
|
680
814
|
return;
|
|
681
815
|
}
|
|
682
816
|
this.#ariaValueNow.write(input, value === null ? null : String(value));
|
|
683
|
-
this.#ariaValueMin.write(input, Number.isFinite(this
|
|
684
|
-
this.#ariaValueMax.write(input, Number.isFinite(this
|
|
817
|
+
this.#ariaValueMin.write(input, Number.isFinite(this.#safeMin) ? String(this.#safeMin) : null);
|
|
818
|
+
this.#ariaValueMax.write(input, Number.isFinite(this.#safeMax) ? String(this.#safeMax) : null);
|
|
685
819
|
}
|
|
686
|
-
/** Returns all custom-spinbutton ARIA leased on `input`. */
|
|
687
820
|
#releaseInputAria(input) {
|
|
688
821
|
this.#ariaValueNow.return(input);
|
|
689
822
|
this.#ariaValueMin.return(input);
|
|
690
823
|
this.#ariaValueMax.return(input);
|
|
691
824
|
}
|
|
692
|
-
/** Disables a step button at its bound, returning focus to the input first. */
|
|
693
825
|
#updateButtons(value) {
|
|
694
|
-
|
|
695
|
-
for (const button of this.
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
826
|
+
const move = this.#move;
|
|
827
|
+
for (const button of this.incrementTargets) {
|
|
828
|
+
this.#toggleButton(button, value < this.#safeMax, move);
|
|
829
|
+
if (!this.#moves.isLatest(move)) return;
|
|
830
|
+
}
|
|
831
|
+
for (const button of this.decrementTargets) {
|
|
832
|
+
this.#toggleButton(button, value > this.#safeMin, move);
|
|
833
|
+
if (!this.#moves.isLatest(move)) return;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
#toggleButton(button, enabled, move) {
|
|
704
837
|
if (enabled) {
|
|
705
838
|
if (button.hasAttribute(OWNED_DISABLED)) {
|
|
706
839
|
button.disabled = false;
|
|
@@ -710,37 +843,64 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
710
843
|
}
|
|
711
844
|
if (button.disabled) return;
|
|
712
845
|
if (document.activeElement === button && this.hasInputTarget) this.inputTarget.focus();
|
|
846
|
+
if (!this.#moves.isLatest(move)) return;
|
|
713
847
|
button.disabled = true;
|
|
714
848
|
button.setAttribute(OWNED_DISABLED, "");
|
|
715
849
|
}
|
|
716
|
-
/** Returns `disabled` only when its explicit marker still identifies controller ownership. */
|
|
717
850
|
#releaseButtonState(button) {
|
|
718
851
|
if (!button.hasAttribute(OWNED_DISABLED)) return;
|
|
719
852
|
button.disabled = false;
|
|
720
853
|
button.removeAttribute(OWNED_DISABLED);
|
|
721
854
|
}
|
|
722
|
-
/** The current numeric value, falling back to a finite min (else 0) when blank. */
|
|
723
855
|
#currentValue() {
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
856
|
+
const text = toHalfWidth(this.inputTarget.value);
|
|
857
|
+
const parsed = Number(text);
|
|
858
|
+
if (Number.isFinite(parsed) && text.trim() !== "") return parsed;
|
|
859
|
+
return Number.isFinite(this.#safeMin) ? this.#safeMin : 0;
|
|
727
860
|
}
|
|
728
|
-
/** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */
|
|
729
861
|
#normalize(raw) {
|
|
730
862
|
return snapSteppedValue(raw, this.#steppedRange);
|
|
731
863
|
}
|
|
732
|
-
/** Shared range configuration; finite endpoints remain allowed off the grid. */
|
|
733
864
|
get #steppedRange() {
|
|
734
|
-
return { min: this
|
|
865
|
+
return { min: this.#safeMin, max: this.#safeMax, step: this.#safeStep };
|
|
866
|
+
}
|
|
867
|
+
get #safeMin() {
|
|
868
|
+
return this.#numbers.read(
|
|
869
|
+
this,
|
|
870
|
+
"min",
|
|
871
|
+
this.minValue,
|
|
872
|
+
_NumberInputController.values.min.default,
|
|
873
|
+
_NumberInputController.valueConstraints.min
|
|
874
|
+
);
|
|
735
875
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
876
|
+
get #safeMax() {
|
|
877
|
+
return this.#numbers.read(
|
|
878
|
+
this,
|
|
879
|
+
"max",
|
|
880
|
+
this.maxValue,
|
|
881
|
+
_NumberInputController.values.max.default,
|
|
882
|
+
_NumberInputController.valueConstraints.max
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
get #safeStep() {
|
|
886
|
+
return this.#numbers.read(
|
|
887
|
+
this,
|
|
888
|
+
"step",
|
|
889
|
+
this.stepValue,
|
|
890
|
+
_NumberInputController.values.step.default,
|
|
891
|
+
_NumberInputController.valueConstraints.step
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
get #safePageStep() {
|
|
895
|
+
return this.#numbers.read(
|
|
896
|
+
this,
|
|
897
|
+
"pageStep",
|
|
898
|
+
this.pageStepValue,
|
|
899
|
+
_NumberInputController.values.pageStep.default,
|
|
900
|
+
_NumberInputController.valueConstraints.pageStep
|
|
901
|
+
);
|
|
741
902
|
}
|
|
742
903
|
};
|
|
743
904
|
|
|
744
905
|
export { NumberInputController };
|
|
745
|
-
//# sourceMappingURL=number_input_controller.js.map
|
|
746
906
|
//# sourceMappingURL=number_input_controller.js.map
|