stimeo-ui 0.16.0 → 0.18.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 +136 -0
- data/README.md +27 -11
- data/dist/cable/index.js +384 -449
- data/dist/controllers/accordion_controller.js +174 -55
- data/dist/controllers/alert_dialog_controller.js +903 -235
- data/dist/controllers/announcer_controller.js +238 -205
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +414 -145
- data/dist/controllers/avatar_controller.js +185 -107
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +282 -58
- data/dist/controllers/calendar_controller.js +228 -260
- data/dist/controllers/carousel_controller.js +536 -436
- data/dist/controllers/character_counter_controller.js +493 -207
- data/dist/controllers/checkbox_controller.js +38 -74
- data/dist/controllers/clipboard_controller.js +261 -149
- data/dist/controllers/collapsible_controller.js +304 -120
- data/dist/controllers/color_picker_controller.js +178 -146
- data/dist/controllers/combobox_controller.js +522 -156
- data/dist/controllers/command_palette_controller.js +1156 -395
- data/dist/controllers/conditional_fields_controller.js +300 -171
- data/dist/controllers/confirm_controller.js +980 -262
- data/dist/controllers/context_menu_controller.js +346 -153
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +281 -158
- data/dist/controllers/currency_input_controller.js +575 -225
- data/dist/controllers/data_grid_controller.js +248 -214
- data/dist/controllers/date_range_picker_controller.js +338 -235
- data/dist/controllers/dialog_controller.js +899 -237
- data/dist/controllers/direct_upload_controller.js +97 -167
- data/dist/controllers/dirty_form_controller.js +72 -69
- data/dist/controllers/dismissible_controller.js +196 -24
- data/dist/controllers/drawer_controller.js +974 -355
- data/dist/controllers/dropdown_controller.js +339 -96
- data/dist/controllers/editable_controller.js +315 -88
- data/dist/controllers/empty_state_controller.js +79 -58
- data/dist/controllers/file_dropzone_controller.js +322 -211
- data/dist/controllers/filter_controller.js +77 -42
- data/dist/controllers/flash_controller.js +238 -347
- data/dist/controllers/focus_controller.js +679 -240
- data/dist/controllers/form_field_controller.js +156 -132
- data/dist/controllers/form_validation_controller.js +31 -97
- data/dist/controllers/frame_loading_controller.js +275 -217
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +327 -130
- data/dist/controllers/idle_controller.js +271 -101
- data/dist/controllers/input_mask_controller.js +483 -97
- data/dist/controllers/intersection_controller.js +150 -106
- data/dist/controllers/lazy_frame_controller.js +24 -68
- data/dist/controllers/listbox_controller.js +523 -240
- data/dist/controllers/local_time_controller.js +102 -77
- data/dist/controllers/masonry_controller.js +80 -96
- data/dist/controllers/menu_controller.js +302 -188
- data/dist/controllers/menubar_controller.js +232 -345
- data/dist/controllers/meter_controller.js +417 -87
- data/dist/controllers/multi_select_controller.js +785 -393
- data/dist/controllers/navigation_menu_controller.js +190 -250
- data/dist/controllers/nested_form_controller.js +396 -192
- data/dist/controllers/network_status_controller.js +268 -62
- data/dist/controllers/number_input_controller.js +610 -303
- data/dist/controllers/optimistic_controller.js +45 -87
- data/dist/controllers/otp_controller.js +658 -255
- data/dist/controllers/overflow_indicator_controller.js +122 -113
- data/dist/controllers/overflow_menu_controller.js +470 -295
- data/dist/controllers/pagination_controller.js +332 -123
- data/dist/controllers/password_reveal_controller.js +392 -121
- data/dist/controllers/password_strength_controller.js +111 -158
- data/dist/controllers/persist_controller.js +165 -111
- data/dist/controllers/pointer_drag_controller.js +434 -188
- data/dist/controllers/popover_controller.js +269 -113
- data/dist/controllers/portal_controller.js +215 -76
- data/dist/controllers/preview_guard_controller.js +243 -116
- data/dist/controllers/progress_controller.js +391 -78
- data/dist/controllers/radio_group_controller.js +278 -211
- data/dist/controllers/range_slider_controller.js +220 -113
- data/dist/controllers/rating_controller.js +458 -211
- data/dist/controllers/read_more_controller.js +138 -63
- data/dist/controllers/reading_progress_controller.js +166 -137
- data/dist/controllers/relative_time_controller.js +211 -112
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
- data/dist/controllers/resizable_controller.js +299 -150
- data/dist/controllers/roving_controller.js +49 -88
- data/dist/controllers/scroll_area_controller.js +490 -246
- data/dist/controllers/scroll_restore_controller.js +14 -55
- data/dist/controllers/scroll_visibility_controller.js +432 -149
- data/dist/controllers/scrollspy_controller.js +193 -248
- data/dist/controllers/separator_controller.js +285 -142
- data/dist/controllers/sidebar_controller.js +1069 -404
- data/dist/controllers/skeleton_controller.js +259 -101
- data/dist/controllers/slider_controller.js +187 -86
- data/dist/controllers/smart_sticky_header_controller.js +173 -66
- data/dist/controllers/sortable_controller.js +26 -125
- data/dist/controllers/spinner_controller.js +288 -173
- data/dist/controllers/step_indicator_controller.js +105 -46
- data/dist/controllers/stepper_controller.js +120 -73
- data/dist/controllers/stick_to_bottom_controller.js +275 -140
- data/dist/controllers/sticky_observer_controller.js +220 -49
- data/dist/controllers/submit_once_controller.js +376 -227
- data/dist/controllers/switch_controller.js +201 -84
- data/dist/controllers/tabs_controller.js +41 -30
- data/dist/controllers/tags_input_controller.js +293 -173
- data/dist/controllers/textarea_autosize_controller.js +108 -74
- data/dist/controllers/theme_controller.js +140 -116
- data/dist/controllers/time_picker_controller.js +331 -127
- data/dist/controllers/toast_controller.js +244 -324
- data/dist/controllers/toggle_group_controller.js +252 -191
- data/dist/controllers/toolbar_controller.js +219 -120
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +283 -369
- data/dist/index.js +8573 -10410
- data/dist/positioning/index.js +154 -47
- data/lib/generators/stimeo/install/install_generator.rb +7 -2
- data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,169 +1,398 @@
|
|
|
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
|
}
|
|
7
|
+
function hasModifierChord(event) {
|
|
8
|
+
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function setAttributeIfChanged(element, name, value, beforeWrite) {
|
|
12
|
+
if (element.getAttribute(name) === value) return false;
|
|
13
|
+
if (value === null) element.removeAttribute(name);
|
|
14
|
+
else element.setAttribute(name, value);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
function sharedRegistry(key, create) {
|
|
19
|
+
const symbol = Symbol.for(key);
|
|
20
|
+
const scope = globalThis;
|
|
21
|
+
const existing = scope[symbol];
|
|
22
|
+
if (existing !== void 0) return existing;
|
|
23
|
+
const registry2 = create();
|
|
24
|
+
Object.defineProperty(globalThis, symbol, { value: registry2 });
|
|
25
|
+
return registry2;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var registry = sharedRegistry(
|
|
29
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
30
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
31
|
+
);
|
|
32
|
+
function isAttributeValue(value) {
|
|
33
|
+
return value === null || typeof value === "string";
|
|
34
|
+
}
|
|
35
|
+
function readLeaseRecord(element, record) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
38
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
39
|
+
const [original, written, ...rest] = parsed;
|
|
40
|
+
const linked = rest[0] !== false;
|
|
41
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
42
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
43
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
44
|
+
} catch {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function attributeStack(element, attribute) {
|
|
49
|
+
const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
50
|
+
registry.stacks.set(element, byAttribute);
|
|
51
|
+
const known = byAttribute.get(attribute);
|
|
52
|
+
if (known) return known;
|
|
53
|
+
const found = [];
|
|
54
|
+
for (const record of registry.names.get(attribute) ?? []) {
|
|
55
|
+
const read = readLeaseRecord(element, record);
|
|
56
|
+
if (read) found.push({ ...read, record });
|
|
57
|
+
}
|
|
58
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
59
|
+
const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
60
|
+
lease: null,
|
|
61
|
+
record,
|
|
62
|
+
written,
|
|
63
|
+
original,
|
|
64
|
+
linked: linked && index > 0
|
|
65
|
+
}));
|
|
66
|
+
byAttribute.set(attribute, stack);
|
|
67
|
+
return stack;
|
|
68
|
+
}
|
|
69
|
+
function findHold(stack, lease, record) {
|
|
70
|
+
return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
|
|
71
|
+
}
|
|
12
72
|
var AttributeLease = class {
|
|
13
73
|
#attribute;
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
74
|
+
#record;
|
|
75
|
+
#base;
|
|
76
|
+
#preserveAuthor;
|
|
77
|
+
#held = /* @__PURE__ */ new Set();
|
|
78
|
+
constructor(attribute, owner, options = {}) {
|
|
17
79
|
this.#attribute = attribute;
|
|
80
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
81
|
+
this.#base = options.base === true;
|
|
82
|
+
this.#preserveAuthor = options.preserveAuthor === true;
|
|
83
|
+
const names = registry.names;
|
|
84
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
18
85
|
}
|
|
19
|
-
/** Writes or removes the leased attribute while preserving its authored value. */
|
|
20
86
|
write(element, value) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
this.#reflect(element, value);
|
|
87
|
+
const stack = attributeStack(element, this.#attribute);
|
|
88
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
89
|
+
this.#held.add(element);
|
|
90
|
+
const shows = !this.#covered(element, stack, hold);
|
|
91
|
+
hold.written = value;
|
|
92
|
+
this.#mark(element, stack);
|
|
93
|
+
if (shows) this.#reflect(element, value);
|
|
31
94
|
}
|
|
32
|
-
/** Returns one lease without overwriting a value subsequently authored by a consumer. */
|
|
33
95
|
return(element) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
this.#
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
96
|
+
const held = this.#held.delete(element);
|
|
97
|
+
const known = registry.stacks.get(element)?.has(this.#attribute);
|
|
98
|
+
const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
|
|
99
|
+
const hold = this.#take(stack);
|
|
100
|
+
if (!hold) return false;
|
|
101
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
102
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
103
|
+
const index = stack.indexOf(hold);
|
|
104
|
+
stack.splice(index, 1);
|
|
105
|
+
const above = stack[index];
|
|
106
|
+
if (above) above.linked &&= hold.linked;
|
|
107
|
+
this.#mark(element, stack);
|
|
108
|
+
if (!owns) return true;
|
|
109
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
110
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
111
|
+
return true;
|
|
45
112
|
}
|
|
46
|
-
/** Returns every outstanding lease using the same ownership check as {@link return}. */
|
|
47
113
|
returnAll() {
|
|
48
|
-
for (const element of Array.from(this.#
|
|
114
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
115
|
+
}
|
|
116
|
+
#take(stack) {
|
|
117
|
+
const hold = findHold(stack, this, this.#record);
|
|
118
|
+
if (hold) hold.lease = this;
|
|
119
|
+
return hold;
|
|
120
|
+
}
|
|
121
|
+
#acquire(element, stack) {
|
|
122
|
+
const current = element.getAttribute(this.#attribute);
|
|
123
|
+
const top = stack.at(-1);
|
|
124
|
+
const bottom = stack[0];
|
|
125
|
+
const over = top?.written === current ? top : void 0;
|
|
126
|
+
const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
|
|
127
|
+
const hold = {
|
|
128
|
+
lease: this,
|
|
129
|
+
record: this.#record,
|
|
130
|
+
written: current,
|
|
131
|
+
original: from ? from.original : current,
|
|
132
|
+
linked: !this.#base && over !== void 0
|
|
133
|
+
};
|
|
134
|
+
if (this.#base && bottom) {
|
|
135
|
+
bottom.linked = true;
|
|
136
|
+
stack.unshift(hold);
|
|
137
|
+
} else stack.push(hold);
|
|
138
|
+
return hold;
|
|
139
|
+
}
|
|
140
|
+
#covered(element, stack, hold) {
|
|
141
|
+
const current = element.getAttribute(this.#attribute);
|
|
142
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
143
|
+
}
|
|
144
|
+
#mark(element, stack) {
|
|
145
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
146
|
+
stack.forEach((hold, index) => {
|
|
147
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
148
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
149
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
150
|
+
records.set(
|
|
151
|
+
hold.record,
|
|
152
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
for (const [name, recorded] of records) {
|
|
156
|
+
setAttributeIfChanged(element, name, recorded);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
#reflect(element, value) {
|
|
160
|
+
setAttributeIfChanged(element, this.#attribute, value);
|
|
49
161
|
}
|
|
50
162
|
};
|
|
51
163
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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;
|
|
164
|
+
var BlurDeferral = class {
|
|
165
|
+
#pending = /* @__PURE__ */ new Map();
|
|
166
|
+
#onRelease;
|
|
167
|
+
constructor(onRelease) {
|
|
168
|
+
this.#onRelease = onRelease;
|
|
64
169
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
170
|
+
get size() {
|
|
171
|
+
return this.#pending.size;
|
|
172
|
+
}
|
|
173
|
+
get elements() {
|
|
174
|
+
return [...this.#pending.keys()];
|
|
175
|
+
}
|
|
176
|
+
has(element) {
|
|
177
|
+
return this.#pending.has(element);
|
|
178
|
+
}
|
|
179
|
+
defer(element) {
|
|
180
|
+
if (this.#pending.has(element)) return;
|
|
181
|
+
const onBlur = () => {
|
|
182
|
+
this.#detach(element);
|
|
183
|
+
this.#onRelease(element);
|
|
184
|
+
};
|
|
185
|
+
this.#pending.set(element, onBlur);
|
|
186
|
+
element.addEventListener("blur", onBlur);
|
|
187
|
+
}
|
|
188
|
+
deferOnly(element) {
|
|
189
|
+
for (const pending of this.elements) {
|
|
190
|
+
if (pending !== element) this.#detach(pending);
|
|
71
191
|
}
|
|
192
|
+
this.defer(element);
|
|
193
|
+
}
|
|
194
|
+
release(element) {
|
|
195
|
+
this.#detach(element);
|
|
72
196
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
197
|
+
releaseAll() {
|
|
198
|
+
for (const element of this.elements) this.#detach(element);
|
|
199
|
+
}
|
|
200
|
+
#detach(element) {
|
|
201
|
+
const onBlur = this.#pending.get(element);
|
|
202
|
+
if (onBlur) element.removeEventListener("blur", onBlur);
|
|
203
|
+
this.#pending.delete(element);
|
|
78
204
|
}
|
|
79
205
|
};
|
|
80
206
|
|
|
81
|
-
// src/utils/composition_tracker.ts
|
|
82
207
|
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
83
208
|
"insertCompositionText",
|
|
84
209
|
"insertFromComposition",
|
|
85
210
|
"deleteCompositionText",
|
|
86
211
|
"deleteByComposition"
|
|
87
212
|
]);
|
|
213
|
+
function isCompositionInputType(inputType) {
|
|
214
|
+
return inputType !== void 0 && COMPOSITION_INPUT_TYPES.has(inputType);
|
|
215
|
+
}
|
|
216
|
+
var BEFORE_MORPH_ATTRIBUTE = "turbo:before-morph-attribute";
|
|
217
|
+
var textOf = (element) => element.value;
|
|
218
|
+
var holdsFocus = (element) => element instanceof Node && element.getRootNode().activeElement === element;
|
|
88
219
|
var CompositionTracker = class {
|
|
89
220
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
90
|
-
#
|
|
221
|
+
#composition = null;
|
|
91
222
|
#onStart;
|
|
92
223
|
#onEnd;
|
|
93
|
-
|
|
224
|
+
#onAbandon;
|
|
94
225
|
#confirmedTarget = null;
|
|
226
|
+
#keyComposing = false;
|
|
227
|
+
#connected = false;
|
|
228
|
+
#abandoned = null;
|
|
95
229
|
constructor(options = {}) {
|
|
96
230
|
this.#onStart = options.onStart;
|
|
97
231
|
this.#onEnd = options.onEnd;
|
|
232
|
+
this.#onAbandon = options.onAbandon;
|
|
98
233
|
}
|
|
99
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
100
234
|
observe(target) {
|
|
235
|
+
this.#connected = true;
|
|
101
236
|
if (this.#observedTargets.has(target)) return;
|
|
102
237
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
103
238
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
104
|
-
target.addEventListener("keydown", this.#handleKeydown);
|
|
239
|
+
target.addEventListener("keydown", this.#handleKeydown, true);
|
|
240
|
+
target.addEventListener("beforeinput", this.#handleBeforeInput, true);
|
|
241
|
+
target.addEventListener("input", this.#handleInput, true);
|
|
242
|
+
target.addEventListener("blur", this.#handleBlur, true);
|
|
243
|
+
target.addEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
|
|
105
244
|
this.#observedTargets.add(target);
|
|
106
245
|
}
|
|
107
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
108
246
|
unobserve(target) {
|
|
109
247
|
if (!this.#observedTargets.delete(target)) return;
|
|
110
|
-
|
|
111
|
-
target
|
|
112
|
-
target.removeEventListener("keydown", this.#handleKeydown);
|
|
113
|
-
this.#activeTargets.delete(target);
|
|
248
|
+
this.#release(target);
|
|
249
|
+
if (this.#composition?.target === target) this.#letGo();
|
|
114
250
|
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
115
251
|
}
|
|
116
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
117
252
|
disconnect() {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
target.removeEventListener("compositionend", this.#handleEnd);
|
|
121
|
-
target.removeEventListener("keydown", this.#handleKeydown);
|
|
122
|
-
}
|
|
253
|
+
this.#connected = false;
|
|
254
|
+
for (const target of this.#observedTargets) this.#release(target);
|
|
123
255
|
this.#observedTargets.clear();
|
|
124
|
-
this.#
|
|
256
|
+
this.#letGo();
|
|
125
257
|
this.#confirmedTarget = null;
|
|
258
|
+
this.#keyComposing = false;
|
|
259
|
+
this.#abandoned = null;
|
|
126
260
|
}
|
|
127
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
128
261
|
isComposing(event) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
262
|
+
this.#settle();
|
|
263
|
+
return event?.isComposing === true || this.#composition !== null;
|
|
264
|
+
}
|
|
265
|
+
composingElement() {
|
|
266
|
+
this.#settle();
|
|
267
|
+
return this.#composition?.element ?? null;
|
|
268
|
+
}
|
|
269
|
+
owns(event) {
|
|
270
|
+
const element = this.composingElement();
|
|
271
|
+
return event.isComposing === true || element !== null && element === event.target;
|
|
272
|
+
}
|
|
137
273
|
consumesConfirmedInput(event) {
|
|
138
274
|
const confirmed = this.#confirmedTarget;
|
|
139
275
|
this.#confirmedTarget = null;
|
|
140
276
|
if (confirmed === null || confirmed !== event.target) return false;
|
|
141
277
|
const inputType = event.inputType;
|
|
142
|
-
return !inputType ||
|
|
278
|
+
return !inputType || isCompositionInputType(inputType);
|
|
279
|
+
}
|
|
280
|
+
#release(target) {
|
|
281
|
+
target.removeEventListener("compositionstart", this.#handleStart);
|
|
282
|
+
target.removeEventListener("compositionend", this.#handleEnd);
|
|
283
|
+
target.removeEventListener("keydown", this.#handleKeydown, true);
|
|
284
|
+
target.removeEventListener("beforeinput", this.#handleBeforeInput, true);
|
|
285
|
+
target.removeEventListener("input", this.#handleInput, true);
|
|
286
|
+
target.removeEventListener("blur", this.#handleBlur, true);
|
|
287
|
+
target.removeEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
|
|
288
|
+
}
|
|
289
|
+
#letGo() {
|
|
290
|
+
const composition = this.#composition;
|
|
291
|
+
if (composition === null) return;
|
|
292
|
+
if (!holdsFocus(composition.element)) {
|
|
293
|
+
this.#composition = null;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
queueMicrotask(() => {
|
|
297
|
+
if (this.#composition !== composition || this.#observedTargets.has(composition.target)) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.#composition = null;
|
|
301
|
+
if (!this.#connected) return;
|
|
302
|
+
this.#abandoned = { element: composition.element, inPlace: true };
|
|
303
|
+
this.#runAbandon();
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
#settle() {
|
|
307
|
+
const composition = this.#composition;
|
|
308
|
+
if (composition === null) return;
|
|
309
|
+
const { target, element, text } = composition;
|
|
310
|
+
if (target instanceof Node && element instanceof Node && !target.contains(element)) {
|
|
311
|
+
this.#composition = null;
|
|
312
|
+
} else if (textOf(element) !== text) {
|
|
313
|
+
this.#abandon(false);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
#abandon(inPlace) {
|
|
317
|
+
this.#owe(inPlace);
|
|
318
|
+
this.#composition = null;
|
|
319
|
+
queueMicrotask(this.#runAbandon);
|
|
320
|
+
}
|
|
321
|
+
#owe(inPlace) {
|
|
322
|
+
const composition = this.#composition;
|
|
323
|
+
if (composition !== null) this.#abandoned = { element: composition.element, inPlace };
|
|
143
324
|
}
|
|
325
|
+
#runAbandon = () => {
|
|
326
|
+
const abandoned = this.#abandoned;
|
|
327
|
+
if (abandoned === null) return;
|
|
328
|
+
this.#abandoned = null;
|
|
329
|
+
this.#onAbandon?.(abandoned.element, abandoned.inPlace);
|
|
330
|
+
};
|
|
144
331
|
#handleStart = (event) => {
|
|
145
332
|
this.#confirmedTarget = null;
|
|
146
|
-
|
|
333
|
+
this.#settle();
|
|
334
|
+
this.#owe(true);
|
|
335
|
+
const target = event.currentTarget;
|
|
336
|
+
const element = event.target;
|
|
337
|
+
this.#composition = { target, element, text: textOf(element) };
|
|
338
|
+
this.#runAbandon();
|
|
147
339
|
this.#onStart?.(event);
|
|
148
340
|
};
|
|
149
341
|
#handleEnd = (event) => {
|
|
150
|
-
|
|
342
|
+
this.#runAbandon();
|
|
343
|
+
if (this.#composition?.target === event.currentTarget) this.#composition = null;
|
|
151
344
|
this.#confirmedTarget = event.target;
|
|
152
345
|
this.#onEnd?.(event);
|
|
153
346
|
};
|
|
154
|
-
|
|
155
|
-
|
|
347
|
+
#handleKeydown = (event) => {
|
|
348
|
+
this.#settle();
|
|
349
|
+
this.#runAbandon();
|
|
156
350
|
this.#confirmedTarget = null;
|
|
351
|
+
this.#keyComposing = event.isComposing === true;
|
|
352
|
+
};
|
|
353
|
+
#handleBeforeInput = (event) => {
|
|
354
|
+
const keyComposing = this.#keyComposing;
|
|
355
|
+
this.#keyComposing = false;
|
|
356
|
+
this.#settle();
|
|
357
|
+
const { isComposing, inputType } = event;
|
|
358
|
+
if (this.#composition !== null && !keyComposing && !isComposing && inputType && !isCompositionInputType(inputType)) {
|
|
359
|
+
this.#abandon(true);
|
|
360
|
+
}
|
|
361
|
+
this.#runAbandon();
|
|
362
|
+
};
|
|
363
|
+
#handleInput = (event) => {
|
|
364
|
+
const composition = this.#composition;
|
|
365
|
+
const { isTrusted, isComposing, inputType } = event;
|
|
366
|
+
if (composition !== null && event.target === composition.element && (isTrusted || isComposing || isCompositionInputType(inputType))) {
|
|
367
|
+
composition.text = textOf(composition.element);
|
|
368
|
+
} else {
|
|
369
|
+
this.#settle();
|
|
370
|
+
}
|
|
371
|
+
this.#runAbandon();
|
|
372
|
+
};
|
|
373
|
+
#handleBlur = (event) => {
|
|
374
|
+
this.#settle();
|
|
375
|
+
this.#runAbandon();
|
|
376
|
+
const composition = this.#composition;
|
|
377
|
+
if (composition === null || composition.target === composition.element) return;
|
|
378
|
+
const { element } = composition;
|
|
379
|
+
if (!(element instanceof Node) || event.target !== element) return;
|
|
380
|
+
const root = element.getRootNode();
|
|
381
|
+
if (root.activeElement !== element) this.#composition = null;
|
|
382
|
+
};
|
|
383
|
+
#handleBeforeMorphAttribute = (event) => {
|
|
384
|
+
this.#settle();
|
|
385
|
+
const composition = this.#composition;
|
|
386
|
+
if (composition?.target !== event.currentTarget || event.target !== composition.element) return;
|
|
387
|
+
const detail = event.detail;
|
|
388
|
+
if (detail?.attributeName === "value") event.preventDefault();
|
|
157
389
|
};
|
|
158
390
|
};
|
|
159
391
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
control.dispatchEvent(new Event("input", { bubbles: true }));
|
|
163
|
-
control.dispatchEvent(new Event("change", { bubbles: true }));
|
|
392
|
+
function commitField(target) {
|
|
393
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
164
394
|
}
|
|
165
395
|
|
|
166
|
-
// src/utils/half_width.ts
|
|
167
396
|
var FULL_WIDTH_SHIFT = 65248;
|
|
168
397
|
function halfWidthChar(char) {
|
|
169
398
|
if (char >= "\uFF01" && char <= "\uFF5E") {
|
|
@@ -177,27 +406,36 @@ function toHalfWidth(text) {
|
|
|
177
406
|
return out;
|
|
178
407
|
}
|
|
179
408
|
|
|
180
|
-
|
|
409
|
+
var ListenerSet = class {
|
|
410
|
+
#abort = new AbortController();
|
|
411
|
+
add(target, type, handler, options) {
|
|
412
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
413
|
+
}
|
|
414
|
+
dispose() {
|
|
415
|
+
this.#abort.abort();
|
|
416
|
+
this.#abort = new AbortController();
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
181
420
|
var MicrotaskCoalescer = class {
|
|
182
421
|
#run;
|
|
183
422
|
#queued = false;
|
|
184
423
|
#active = false;
|
|
185
424
|
#generation = 0;
|
|
186
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
187
425
|
constructor(run) {
|
|
188
426
|
this.#run = run;
|
|
189
427
|
}
|
|
190
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
191
428
|
activate() {
|
|
192
429
|
this.#active = true;
|
|
193
430
|
}
|
|
194
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
195
431
|
cancel() {
|
|
196
432
|
this.#active = false;
|
|
197
433
|
this.#queued = false;
|
|
198
434
|
this.#generation += 1;
|
|
199
435
|
}
|
|
200
|
-
|
|
436
|
+
get pending() {
|
|
437
|
+
return this.#queued;
|
|
438
|
+
}
|
|
201
439
|
schedule() {
|
|
202
440
|
if (!this.#active || this.#queued) return;
|
|
203
441
|
this.#queued = true;
|
|
@@ -210,43 +448,110 @@ var MicrotaskCoalescer = class {
|
|
|
210
448
|
}
|
|
211
449
|
};
|
|
212
450
|
|
|
213
|
-
|
|
451
|
+
var MorphRenderWatcher = class {
|
|
452
|
+
#listeners = new ListenerSet();
|
|
453
|
+
#pass;
|
|
454
|
+
#element = null;
|
|
455
|
+
#onMorph = () => this.#pass.schedule();
|
|
456
|
+
constructor(run) {
|
|
457
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
458
|
+
}
|
|
459
|
+
observe(element) {
|
|
460
|
+
if (this.#element === element) return;
|
|
461
|
+
this.disconnect();
|
|
462
|
+
this.#element = element;
|
|
463
|
+
this.#pass.activate();
|
|
464
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
465
|
+
}
|
|
466
|
+
schedule() {
|
|
467
|
+
this.#pass.schedule();
|
|
468
|
+
}
|
|
469
|
+
get pending() {
|
|
470
|
+
return this.#pass.pending;
|
|
471
|
+
}
|
|
472
|
+
disconnect() {
|
|
473
|
+
this.#listeners.dispose();
|
|
474
|
+
this.#pass.cancel();
|
|
475
|
+
this.#element = null;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
var MoveCounter = class {
|
|
480
|
+
#sequence = 0;
|
|
481
|
+
record() {
|
|
482
|
+
this.#sequence += 1;
|
|
483
|
+
return this.#sequence;
|
|
484
|
+
}
|
|
485
|
+
isLatest(token) {
|
|
486
|
+
return token === this.#sequence;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
var NUMBER_BOUNDS = {
|
|
491
|
+
nonNegative: { finite: true, min: 0 },
|
|
492
|
+
positive: { finite: true, exclusiveMin: 0 },
|
|
493
|
+
lowerBound: { finite: true, allowInfinity: "negative" },
|
|
494
|
+
upperBound: { finite: true, allowInfinity: "positive" }};
|
|
495
|
+
function matchesNumberBounds(value, bounds) {
|
|
496
|
+
if (!Number.isFinite(value)) {
|
|
497
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
498
|
+
if (direction === null) return false;
|
|
499
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
500
|
+
}
|
|
501
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
502
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
503
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
504
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
505
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function readNumber(raw, fallback, bounds) {
|
|
510
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
var NumberValueReader = class {
|
|
514
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
515
|
+
read(owner, name, raw, fallback, bounds) {
|
|
516
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
517
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
518
|
+
this.#lastRejected.delete(name);
|
|
519
|
+
return resolved;
|
|
520
|
+
}
|
|
521
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
522
|
+
const literal = owner.element.getAttribute(attribute);
|
|
523
|
+
if (literal === null) {
|
|
524
|
+
this.#lastRejected.delete(name);
|
|
525
|
+
return resolved;
|
|
526
|
+
}
|
|
527
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
528
|
+
this.#lastRejected.set(name, literal);
|
|
529
|
+
console.warn(
|
|
530
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
return resolved;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
214
537
|
var TimerRegistry = class {
|
|
215
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
216
538
|
ids = /* @__PURE__ */ new Set();
|
|
217
|
-
/**
|
|
218
|
-
* Cancels a single tracked timer.
|
|
219
|
-
*
|
|
220
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
221
|
-
* registry), so callers can clear defensively without guarding.
|
|
222
|
-
*/
|
|
223
539
|
clear(id) {
|
|
224
540
|
if (this.ids.delete(id)) {
|
|
225
541
|
this.cancel(id);
|
|
226
542
|
}
|
|
227
543
|
}
|
|
228
|
-
/**
|
|
229
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
230
|
-
* to guarantee no timer outlives the element.
|
|
231
|
-
*/
|
|
232
544
|
clearAll() {
|
|
233
545
|
for (const id of this.ids) {
|
|
234
546
|
this.cancel(id);
|
|
235
547
|
}
|
|
236
548
|
this.ids.clear();
|
|
237
549
|
}
|
|
238
|
-
/** Number of timers currently tracked (pending). */
|
|
239
550
|
get size() {
|
|
240
551
|
return this.ids.size;
|
|
241
552
|
}
|
|
242
553
|
};
|
|
243
554
|
var SafeTimeout = class extends TimerRegistry {
|
|
244
|
-
/**
|
|
245
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
246
|
-
*
|
|
247
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
248
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
249
|
-
*/
|
|
250
555
|
set(callback, delay) {
|
|
251
556
|
const id = this.schedule(() => {
|
|
252
557
|
this.ids.delete(id);
|
|
@@ -263,7 +568,6 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
263
568
|
}
|
|
264
569
|
};
|
|
265
570
|
var SafeInterval = class extends TimerRegistry {
|
|
266
|
-
/** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
|
|
267
571
|
set(callback, delay) {
|
|
268
572
|
const id = this.schedule(callback, delay);
|
|
269
573
|
this.ids.add(id);
|
|
@@ -277,7 +581,6 @@ var SafeInterval = class extends TimerRegistry {
|
|
|
277
581
|
}
|
|
278
582
|
};
|
|
279
583
|
|
|
280
|
-
// src/utils/stepped_value.ts
|
|
281
584
|
function effectiveStep(step) {
|
|
282
585
|
return Number.isFinite(step) && step > 0 ? step : 1;
|
|
283
586
|
}
|
|
@@ -383,9 +686,12 @@ function finiteFallback(min, max) {
|
|
|
383
686
|
return 0;
|
|
384
687
|
}
|
|
385
688
|
|
|
386
|
-
// src/controllers/number_input_controller.ts
|
|
387
689
|
var OWNED_DISABLED = "data-number-input-disabled";
|
|
690
|
+
var sameRange = (shown, declared) => shown?.min === declared.min && shown.max === declared.max && shown.step === declared.step;
|
|
388
691
|
var NumberInputController = class _NumberInputController extends Controller {
|
|
692
|
+
#numbers = new NumberValueReader();
|
|
693
|
+
#moves = new MoveCounter();
|
|
694
|
+
#move = 0;
|
|
389
695
|
static targets = ["input", "increment", "decrement"];
|
|
390
696
|
static values = {
|
|
391
697
|
min: { type: Number, default: Number.NEGATIVE_INFINITY },
|
|
@@ -393,93 +699,70 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
393
699
|
step: { type: Number, default: 1 },
|
|
394
700
|
pageStep: { type: Number, default: 0 }
|
|
395
701
|
};
|
|
702
|
+
static valueConstraints = {
|
|
703
|
+
min: NUMBER_BOUNDS.lowerBound,
|
|
704
|
+
max: NUMBER_BOUNDS.upperBound,
|
|
705
|
+
step: NUMBER_BOUNDS.positive,
|
|
706
|
+
pageStep: NUMBER_BOUNDS.nonNegative
|
|
707
|
+
};
|
|
396
708
|
static actions = ["decrement", "increment", "onInput", "onKeydown"];
|
|
397
709
|
static events = ["change", "reconcile"];
|
|
398
|
-
/** Delay (ms) a button must be held before auto-repeat starts. */
|
|
399
710
|
static #HOLD_DELAY_MS = 400;
|
|
400
|
-
/** Interval (ms) between auto-repeat steps once a hold has started. */
|
|
401
711
|
static #HOLD_REPEAT_MS = 80;
|
|
402
|
-
/**
|
|
403
|
-
* Window (ms) after a hold ends during which the trailing synthetic `click` is
|
|
404
|
-
* swallowed. A safety net for the (rare) case where that click never arrives
|
|
405
|
-
* (e.g. the pointer was released off the button): the flag self-clears instead
|
|
406
|
-
* of poisoning the next legitimate click.
|
|
407
|
-
*/
|
|
408
712
|
static #SUPPRESS_RESET_MS = 250;
|
|
409
|
-
|
|
410
|
-
#globalGuards = null;
|
|
411
|
-
/** Per-target guards allow an old button to be released without touching its replacement. */
|
|
713
|
+
#listeners = new ListenerSet();
|
|
412
714
|
#buttonGuards = /* @__PURE__ */ new Map();
|
|
413
|
-
/**
|
|
414
|
-
* Tracks IME lifecycle on the current input, including confirming keys without a
|
|
415
|
-
* signal, and runs a reconciliation the composition held back once it ends.
|
|
416
|
-
*/
|
|
417
715
|
#composition = new CompositionTracker({
|
|
418
|
-
onEnd: () => {
|
|
419
|
-
if (this
|
|
716
|
+
onEnd: (event) => {
|
|
717
|
+
if (this.hasInputTarget && event.target === this.inputTarget) {
|
|
718
|
+
this.#recordDraft(this.inputTarget);
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
onAbandon: () => {
|
|
722
|
+
if (this.#reconcileHeld) this.#reconcile();
|
|
420
723
|
}
|
|
421
724
|
});
|
|
422
|
-
/** A reconciliation that arrived while the input was composing. */
|
|
423
725
|
#reconcileHeld = false;
|
|
424
|
-
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
#ariaValueMin = new AttributeLease("aria-valuemin");
|
|
428
|
-
#ariaValueMax = new AttributeLease("aria-valuemax");
|
|
429
|
-
/**
|
|
430
|
-
* The number shown last — reconciled or committed by the user — which the next
|
|
431
|
-
* move is measured from; `null` while the field is blank, and before this
|
|
432
|
-
* instance has shown one. A disconnect keeps it for the same instance
|
|
433
|
-
* connecting again.
|
|
434
|
-
*/
|
|
726
|
+
#ariaValueNow = new AttributeLease("aria-valuenow", this.identifier);
|
|
727
|
+
#ariaValueMin = new AttributeLease("aria-valuemin", this.identifier);
|
|
728
|
+
#ariaValueMax = new AttributeLease("aria-valuemax", this.identifier);
|
|
435
729
|
#lastValue = null;
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
730
|
+
#accepted = null;
|
|
731
|
+
#draft = null;
|
|
732
|
+
#parked = new BlurDeferral((input) => {
|
|
733
|
+
if (document.activeElement === input) this.#parked.defer(input);
|
|
734
|
+
else if (this.#draft?.input === input) this.#draft = null;
|
|
735
|
+
});
|
|
736
|
+
#shown = null;
|
|
440
737
|
#connected = false;
|
|
441
|
-
/** Timers for the hold delay and the suppress-reset safety net. */
|
|
442
738
|
#holdTimeouts = new SafeTimeout();
|
|
443
|
-
/** The running auto-repeat interval (one at a time). */
|
|
444
739
|
#holdIntervals = new SafeInterval();
|
|
445
|
-
/** True while a hold is armed/running, making `#stopHold` idempotent. */
|
|
446
740
|
#holdActive = false;
|
|
447
|
-
|
|
741
|
+
#holdGeneration = 0;
|
|
448
742
|
#repeatedDuringHold = false;
|
|
449
|
-
/** True when the next `click` is the trailing one after a hold and must be ignored. */
|
|
450
743
|
#suppressNextClick = false;
|
|
451
|
-
/** Button and pointer that own the current hold, so other instances/pointers cannot end it. */
|
|
452
744
|
#holdButton = null;
|
|
453
745
|
#holdPointerId = null;
|
|
454
|
-
|
|
455
|
-
#repaint = new MicrotaskCoalescer(() => this.#reconcile());
|
|
456
|
-
/** Normalizes any initial value and wires the focus/hold pointer guards. */
|
|
746
|
+
#repaint = new MorphRenderWatcher(() => this.#reconcile());
|
|
457
747
|
connect() {
|
|
458
|
-
this.#repaint.
|
|
459
|
-
this.#beforeCache.activate();
|
|
460
|
-
this.#globalGuards = new AbortController();
|
|
461
|
-
const { signal } = this.#globalGuards;
|
|
748
|
+
this.#repaint.observe(this.element);
|
|
462
749
|
for (const button of this.incrementTargets) this.#wireButton(button, 1);
|
|
463
750
|
for (const button of this.decrementTargets) this.#wireButton(button, -1);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
751
|
+
this.#listeners.add(window, "pointerup", this.#onPointerEnd);
|
|
752
|
+
this.#listeners.add(window, "pointercancel", this.#onPointerEnd);
|
|
753
|
+
this.#listeners.add(window, "blur", this.#onWindowBlur);
|
|
754
|
+
this.#listeners.add(this.element, "input", this.#onDraftInput);
|
|
755
|
+
this.#listeners.add(this.element, "turbo:before-morph-attribute", this.#onBeforeMorphAttribute);
|
|
467
756
|
this.#reconcile();
|
|
468
757
|
this.#connected = true;
|
|
469
758
|
}
|
|
470
|
-
/**
|
|
471
|
-
* Releases listeners, derived attributes, timers, and the hold state. The number
|
|
472
|
-
* shown last stays: the pass the next `connect()` runs takes a new one without a
|
|
473
|
-
* report from an input that is present, and with none it stays the number an
|
|
474
|
-
* input arriving later is compared with.
|
|
475
|
-
*/
|
|
476
759
|
disconnect() {
|
|
477
760
|
this.#connected = false;
|
|
478
|
-
this.#repaint.
|
|
761
|
+
this.#repaint.disconnect();
|
|
479
762
|
this.#composition.disconnect();
|
|
480
|
-
this.#
|
|
481
|
-
this.#
|
|
482
|
-
this.#
|
|
763
|
+
this.#listeners.dispose();
|
|
764
|
+
if (this.#draft?.input !== document.activeElement) this.#draft = null;
|
|
765
|
+
if (this.#draft !== null) this.#parked.deferOnly(this.#draft.input);
|
|
483
766
|
const buttons = /* @__PURE__ */ new Set([
|
|
484
767
|
...this.#buttonGuards.keys(),
|
|
485
768
|
...this.incrementTargets,
|
|
@@ -492,74 +775,78 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
492
775
|
this.#repeatedDuringHold = false;
|
|
493
776
|
this.#suppressNextClick = false;
|
|
494
777
|
this.#holdTimeouts.clearAll();
|
|
495
|
-
this.#holdIntervals.clearAll();
|
|
496
778
|
this.#ariaValueNow.returnAll();
|
|
497
779
|
this.#ariaValueMin.returnAll();
|
|
498
780
|
this.#ariaValueMax.returnAll();
|
|
499
781
|
}
|
|
500
|
-
/** Reconciles the value against a minimum changed by application code or a Turbo morph. */
|
|
501
782
|
minValueChanged() {
|
|
502
783
|
this.#repaint.schedule();
|
|
503
784
|
}
|
|
504
|
-
/** Reconciles the value against a maximum changed by application code or a Turbo morph. */
|
|
505
785
|
maxValueChanged() {
|
|
506
786
|
this.#repaint.schedule();
|
|
507
787
|
}
|
|
508
|
-
/** Reconciles the value against a step changed by application code or a Turbo morph. */
|
|
509
788
|
stepValueChanged() {
|
|
510
789
|
this.#repaint.schedule();
|
|
511
790
|
}
|
|
512
|
-
/** Tracks and reconciles an input inserted or replaced at runtime. */
|
|
513
791
|
inputTargetConnected(input) {
|
|
514
792
|
this.#composition.observe(input);
|
|
515
793
|
this.#repaint.schedule();
|
|
516
794
|
}
|
|
517
|
-
/**
|
|
518
|
-
* Releases ARIA and composition state owned by an input that left the controller.
|
|
519
|
-
* The number it showed stays the baseline, so an input swapped in holding another
|
|
520
|
-
* number is reported as `reconcile`.
|
|
521
|
-
*/
|
|
522
795
|
inputTargetDisconnected(input) {
|
|
796
|
+
const composing = this.#composition.composingElement() === input;
|
|
523
797
|
this.#composition.unobserve(input);
|
|
798
|
+
if (composing && this.#composition.composingElement() === input && this.inputTargets.includes(input)) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
524
801
|
this.#releaseInputAria(input);
|
|
525
802
|
this.#stopHold(false);
|
|
526
803
|
this.#repaint.schedule();
|
|
527
804
|
}
|
|
528
|
-
/** Wires focus preservation and hold behavior on a runtime increment target. */
|
|
529
805
|
incrementTargetConnected(button) {
|
|
530
806
|
this.#wireButton(button, 1);
|
|
531
807
|
this.#repaint.schedule();
|
|
532
808
|
}
|
|
533
|
-
/** Releases only the increment target that actually disconnected. */
|
|
534
809
|
incrementTargetDisconnected(button) {
|
|
535
810
|
this.#unwireButton(button);
|
|
536
|
-
this.#repaint.schedule();
|
|
537
811
|
}
|
|
538
|
-
/** Wires focus preservation and hold behavior on a runtime decrement target. */
|
|
539
812
|
decrementTargetConnected(button) {
|
|
540
813
|
this.#wireButton(button, -1);
|
|
541
814
|
this.#repaint.schedule();
|
|
542
815
|
}
|
|
543
|
-
/** Releases only the decrement target that actually disconnected. */
|
|
544
816
|
decrementTargetDisconnected(button) {
|
|
545
817
|
this.#unwireButton(button);
|
|
546
|
-
this.#repaint.schedule();
|
|
547
818
|
}
|
|
548
|
-
/** Increases by one step. Bound via `data-action` (click). */
|
|
549
819
|
increment() {
|
|
550
820
|
if (this.#consumeSuppressedClick()) return;
|
|
551
821
|
if (!this.hasInputTarget) return;
|
|
552
822
|
this.#commitStep(1);
|
|
553
823
|
this.inputTarget.focus();
|
|
554
824
|
}
|
|
555
|
-
/** Decreases by one step. Bound via `data-action` (click). */
|
|
556
825
|
decrement() {
|
|
557
826
|
if (this.#consumeSuppressedClick()) return;
|
|
558
827
|
if (!this.hasInputTarget) return;
|
|
559
828
|
this.#commitStep(-1);
|
|
560
829
|
this.inputTarget.focus();
|
|
561
830
|
}
|
|
562
|
-
|
|
831
|
+
#onDraftInput = (event) => {
|
|
832
|
+
if (!this.hasInputTarget) return;
|
|
833
|
+
const input = this.inputTarget;
|
|
834
|
+
if (event.target !== input || document.activeElement !== input) return;
|
|
835
|
+
this.#recordDraft(input);
|
|
836
|
+
};
|
|
837
|
+
#recordDraft(input) {
|
|
838
|
+
this.#draft = this.#accepted?.input === input && input.value !== this.#accepted.text ? { input, text: input.value } : null;
|
|
839
|
+
if (this.#draft === null) this.#resumeHeldReconcile();
|
|
840
|
+
}
|
|
841
|
+
#onBeforeMorphAttribute = (event) => {
|
|
842
|
+
if (!this.hasInputTarget || event.target !== this.inputTarget) return;
|
|
843
|
+
const detail = event.detail;
|
|
844
|
+
if (detail?.attributeName === "value" && this.#isEditing(this.inputTarget))
|
|
845
|
+
event.preventDefault();
|
|
846
|
+
};
|
|
847
|
+
#isEditing(input) {
|
|
848
|
+
return this.#draft?.input === input && this.#accepted?.input === input && document.activeElement === input && input.value === this.#draft.text && input.value !== this.#accepted.text;
|
|
849
|
+
}
|
|
563
850
|
onInput() {
|
|
564
851
|
if (!this.hasInputTarget) return;
|
|
565
852
|
if (this.inputTarget.value.trim() === "") {
|
|
@@ -569,11 +856,12 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
569
856
|
}
|
|
570
857
|
this.#commit(this.#currentValue());
|
|
571
858
|
}
|
|
572
|
-
/** Keyboard stepping per the APG spinbutton model. */
|
|
573
859
|
onKeydown(event) {
|
|
574
860
|
if (!this.hasInputTarget) return;
|
|
575
861
|
if (this.#composition.isComposing(event)) return;
|
|
576
862
|
if (isReservedArrowChord(event)) return;
|
|
863
|
+
if ((event.key === "Home" || event.key === "End" || event.key === "PageUp" || event.key === "PageDown") && hasModifierChord(event))
|
|
864
|
+
return;
|
|
577
865
|
let next = null;
|
|
578
866
|
switch (event.key) {
|
|
579
867
|
case "ArrowUp":
|
|
@@ -583,18 +871,18 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
583
871
|
next = stepSteppedValue(this.#currentValue(), -1, this.#steppedRange);
|
|
584
872
|
break;
|
|
585
873
|
case "PageUp":
|
|
586
|
-
next = this
|
|
874
|
+
next = this.#safePageStep > 0 ? this.#currentValue() + this.#safePageStep : stepSteppedValue(this.#currentValue(), 10, this.#steppedRange);
|
|
587
875
|
break;
|
|
588
876
|
case "PageDown":
|
|
589
|
-
next = this
|
|
877
|
+
next = this.#safePageStep > 0 ? this.#currentValue() - this.#safePageStep : stepSteppedValue(this.#currentValue(), -10, this.#steppedRange);
|
|
590
878
|
break;
|
|
591
879
|
case "Home":
|
|
592
|
-
if (!Number.isFinite(this
|
|
593
|
-
next = this
|
|
880
|
+
if (!Number.isFinite(this.#safeMin)) return;
|
|
881
|
+
next = this.#safeMin;
|
|
594
882
|
break;
|
|
595
883
|
case "End":
|
|
596
|
-
if (!Number.isFinite(this
|
|
597
|
-
next = this
|
|
884
|
+
if (!Number.isFinite(this.#safeMax)) return;
|
|
885
|
+
next = this.#safeMax;
|
|
598
886
|
break;
|
|
599
887
|
default:
|
|
600
888
|
return;
|
|
@@ -602,11 +890,6 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
602
890
|
event.preventDefault();
|
|
603
891
|
this.#commit(next, true);
|
|
604
892
|
}
|
|
605
|
-
/**
|
|
606
|
-
* Wires a step button: `pointerdown` keeps focus on the input and arms the
|
|
607
|
-
* hold; leaving the button while held stops it (the global listeners cover
|
|
608
|
-
* release/cancel/blur).
|
|
609
|
-
*/
|
|
610
893
|
#wireButton(button, direction) {
|
|
611
894
|
if (this.#buttonGuards.has(button)) return;
|
|
612
895
|
const guard = new AbortController();
|
|
@@ -622,14 +905,12 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
622
905
|
{ signal: guard.signal }
|
|
623
906
|
);
|
|
624
907
|
}
|
|
625
|
-
/** Aborts one target's listeners and returns any state derived onto that button. */
|
|
626
908
|
#unwireButton(button) {
|
|
627
909
|
this.#buttonGuards.get(button)?.abort();
|
|
628
910
|
this.#buttonGuards.delete(button);
|
|
629
911
|
if (this.#holdButton === button) this.#stopHold(false);
|
|
630
912
|
this.#releaseButtonState(button);
|
|
631
913
|
}
|
|
632
|
-
/** Starts a hold: focus the input, then schedule the first repeat after a delay. */
|
|
633
914
|
#armHold(event, button, direction) {
|
|
634
915
|
const pointerButton = event.button;
|
|
635
916
|
if (typeof pointerButton === "number" && pointerButton !== 0) return;
|
|
@@ -642,25 +923,19 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
642
923
|
this.#holdPointerId = this.#pointerId(event);
|
|
643
924
|
this.#repeatedDuringHold = false;
|
|
644
925
|
this.#suppressNextClick = false;
|
|
926
|
+
const generation = ++this.#holdGeneration;
|
|
645
927
|
this.#holdTimeouts.set(() => {
|
|
646
|
-
if (!this.#commitStep(direction))
|
|
647
|
-
this.#stopHold();
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
928
|
+
if (!this.#commitStep(direction) || generation !== this.#holdGeneration) return;
|
|
650
929
|
this.#repeatedDuringHold = true;
|
|
651
930
|
this.#holdIntervals.set(() => {
|
|
652
931
|
if (!this.#commitStep(direction)) this.#stopHold();
|
|
653
932
|
}, _NumberInputController.#HOLD_REPEAT_MS);
|
|
654
933
|
}, _NumberInputController.#HOLD_DELAY_MS);
|
|
655
934
|
}
|
|
656
|
-
/**
|
|
657
|
-
* Stops the current hold (idempotent). When the hold actually repeated, the
|
|
658
|
-
* trailing synthetic `click` must be ignored, so it is suppressed until the
|
|
659
|
-
* click consumes it or a short safety-net timeout clears it.
|
|
660
|
-
*/
|
|
661
935
|
#stopHold(suppressTrailingClick = true) {
|
|
662
936
|
if (!this.#holdActive) return;
|
|
663
937
|
this.#holdActive = false;
|
|
938
|
+
this.#holdGeneration += 1;
|
|
664
939
|
this.#holdButton = null;
|
|
665
940
|
this.#holdPointerId = null;
|
|
666
941
|
this.#holdTimeouts.clearAll();
|
|
@@ -670,133 +945,145 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
670
945
|
this.#holdTimeouts.set(() => {
|
|
671
946
|
this.#suppressNextClick = false;
|
|
672
947
|
}, _NumberInputController.#SUPPRESS_RESET_MS);
|
|
673
|
-
} else if (!suppressTrailingClick) {
|
|
674
|
-
this.#suppressNextClick = false;
|
|
675
948
|
}
|
|
676
949
|
}
|
|
677
|
-
/** Ends a hold only for its initiating pointer (legacy/synthetic events remain accepted). */
|
|
678
950
|
#onPointerEnd = (event) => {
|
|
679
951
|
if (this.#ownsPointer(event)) this.#stopHold();
|
|
680
952
|
};
|
|
681
|
-
/** Window deactivation is global and always ends the active hold. */
|
|
682
953
|
#onWindowBlur = () => {
|
|
683
954
|
this.#stopHold();
|
|
684
955
|
};
|
|
685
|
-
/** Extracts pointer identity when the runtime event exposes it. */
|
|
686
956
|
#pointerId(event) {
|
|
687
957
|
return event.pointerId ?? null;
|
|
688
958
|
}
|
|
689
|
-
/** Whether `event` belongs to the active pointer, with a synthetic-event fallback. */
|
|
690
959
|
#ownsPointer(event) {
|
|
691
960
|
const pointerId = this.#pointerId(event);
|
|
692
961
|
return this.#holdActive && (this.#holdPointerId === null || pointerId === null || pointerId === this.#holdPointerId);
|
|
693
962
|
}
|
|
694
|
-
/** Consumes a pending trailing-click suppression; returns true if the click was swallowed. */
|
|
695
963
|
#consumeSuppressedClick() {
|
|
696
964
|
if (!this.#suppressNextClick) return false;
|
|
697
965
|
this.#suppressNextClick = false;
|
|
698
966
|
this.#holdTimeouts.clearAll();
|
|
699
967
|
return true;
|
|
700
968
|
}
|
|
701
|
-
/**
|
|
702
|
-
* Normalizes `raw`, reflects it, and dispatches `{ value: number }` only when
|
|
703
|
-
* the committed numeric value differs from the last reconciled value.
|
|
704
|
-
*
|
|
705
|
-
* @returns Whether the value changed (drives the auto-repeat's bound stop).
|
|
706
|
-
*/
|
|
707
969
|
#commit(raw, report = false) {
|
|
708
970
|
const value = this.#normalize(raw);
|
|
709
971
|
const changed = this.#lastValue === null || value !== this.#lastValue;
|
|
710
972
|
const shown = this.inputTarget.value;
|
|
711
|
-
this.#write(value);
|
|
712
973
|
this.#lastValue = value;
|
|
713
|
-
if (
|
|
974
|
+
if (changed) this.#move = this.#moves.record();
|
|
975
|
+
const move = this.#move;
|
|
976
|
+
this.#write(value);
|
|
977
|
+
if (!this.#moves.isLatest(move)) return changed;
|
|
978
|
+
if (report && shown !== this.inputTarget.value) {
|
|
979
|
+
const input = this.inputTarget;
|
|
980
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
981
|
+
if (!this.#moves.isLatest(move)) return changed;
|
|
982
|
+
commitField(input);
|
|
983
|
+
}
|
|
984
|
+
if (!this.#moves.isLatest(move)) return changed;
|
|
714
985
|
if (changed) this.dispatch("change", { detail: { value } });
|
|
715
986
|
return changed;
|
|
716
987
|
}
|
|
717
|
-
/** Commits an adjacent endpoint/grid value and reports whether it moved. */
|
|
718
988
|
#commitStep(count) {
|
|
719
989
|
if (!this.hasInputTarget) return false;
|
|
720
990
|
return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange), true);
|
|
721
991
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
* composing, its text is the IME's: the pass waits for the composition to end
|
|
726
|
-
* instead of reading and rewriting it. With no input there is nothing to show,
|
|
727
|
-
* and the number shown last stays the baseline for the input that comes next.
|
|
728
|
-
*
|
|
729
|
-
* @stimeoRenderRoot
|
|
730
|
-
*/
|
|
992
|
+
#resumeHeldReconcile() {
|
|
993
|
+
if (this.#reconcileHeld) this.#repaint.schedule();
|
|
994
|
+
}
|
|
731
995
|
#reconcile() {
|
|
732
|
-
if (!this.hasInputTarget)
|
|
733
|
-
|
|
734
|
-
this.#
|
|
996
|
+
if (!this.hasInputTarget) {
|
|
997
|
+
if (this.#draft?.input !== document.activeElement) this.#draft = null;
|
|
998
|
+
if (this.#draft !== null) this.#parked.deferOnly(this.#draft.input);
|
|
735
999
|
return;
|
|
736
1000
|
}
|
|
737
|
-
this.#
|
|
1001
|
+
this.#parked.releaseAll();
|
|
738
1002
|
const previous = this.#lastValue;
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
this.#
|
|
742
|
-
this.#
|
|
1003
|
+
const input = this.inputTarget;
|
|
1004
|
+
if (this.#composition.composingElement() === input || this.#isEditing(input)) {
|
|
1005
|
+
const shown = this.#shown?.input === input ? this.#shown : null;
|
|
1006
|
+
this.#reconcileHeld = shown === null || !sameRange(shown.range, this.#steppedRange);
|
|
1007
|
+
if (shown !== null) this.#show(shown.value, shown.at, shown.range);
|
|
743
1008
|
} else {
|
|
744
|
-
this.#
|
|
745
|
-
this.#
|
|
1009
|
+
this.#draft = null;
|
|
1010
|
+
this.#reconcileHeld = false;
|
|
1011
|
+
const shown = this.#shown;
|
|
1012
|
+
if (shown !== null && this.#accepted?.input === input && input.value === this.#accepted.text && sameRange(shown.range, this.#steppedRange)) {
|
|
1013
|
+
const move = this.#move;
|
|
1014
|
+
this.#show(shown.value, shown.at, shown.range);
|
|
1015
|
+
if (!this.#moves.isLatest(move)) return;
|
|
1016
|
+
} else if (input.value.trim() !== "") {
|
|
1017
|
+
const value = this.#normalize(this.#currentValue());
|
|
1018
|
+
this.#lastValue = value;
|
|
1019
|
+
const move = this.#move;
|
|
1020
|
+
this.#write(value, true);
|
|
1021
|
+
if (!this.#moves.isLatest(move)) return;
|
|
1022
|
+
} else {
|
|
1023
|
+
this.#lastValue = null;
|
|
1024
|
+
this.#reflectEmpty();
|
|
1025
|
+
}
|
|
746
1026
|
}
|
|
747
1027
|
this.#reportMove(previous);
|
|
748
1028
|
}
|
|
749
|
-
/**
|
|
750
|
-
* Reports the number shown as `reconcile` when a change the page made moved it
|
|
751
|
-
* from `previous`. The page moved it, so it is reported apart from `change`,
|
|
752
|
-
* which stays reserved for user edits. A blank field reports nothing, the way a
|
|
753
|
-
* user blanking it does: both events carry a number.
|
|
754
|
-
*/
|
|
755
1029
|
#reportMove(previous) {
|
|
756
1030
|
const value = this.#lastValue;
|
|
757
1031
|
if (!this.#connected || value === null || value === previous) return;
|
|
758
1032
|
this.dispatch("reconcile", { detail: { value } });
|
|
759
1033
|
}
|
|
760
|
-
|
|
761
|
-
#write(value) {
|
|
1034
|
+
#write(value, keepCaret = false) {
|
|
762
1035
|
const input = this.inputTarget;
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1036
|
+
const text = String(value);
|
|
1037
|
+
if (input.value !== text) {
|
|
1038
|
+
const caret = keepCaret && input === document.activeElement ? input.selectionStart : null;
|
|
1039
|
+
input.value = text;
|
|
1040
|
+
if (caret !== null) {
|
|
1041
|
+
const position = Math.min(caret, text.length);
|
|
1042
|
+
input.setSelectionRange(position, position);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
this.#draft = null;
|
|
1046
|
+
this.#reconcileHeld = false;
|
|
1047
|
+
this.#accepted = { input, text };
|
|
1048
|
+
this.#show(value, value);
|
|
1049
|
+
}
|
|
1050
|
+
#show(value, at, range = this.#steppedRange) {
|
|
1051
|
+
this.#shown = { input: this.inputTarget, value, at, range };
|
|
1052
|
+
this.#syncInputAria(this.inputTarget, value, range);
|
|
1053
|
+
this.#updateButtons(at, range);
|
|
766
1054
|
}
|
|
767
|
-
/** Reflects a blank value without inventing a numeric `aria-valuenow`. */
|
|
768
1055
|
#reflectEmpty() {
|
|
769
|
-
this.#
|
|
770
|
-
this.#
|
|
1056
|
+
this.#draft = null;
|
|
1057
|
+
this.#reconcileHeld = false;
|
|
1058
|
+
this.#accepted = { input: this.inputTarget, text: this.inputTarget.value };
|
|
1059
|
+
this.#show(null, this.#currentValue());
|
|
771
1060
|
}
|
|
772
|
-
|
|
773
|
-
#syncInputAria(input, value) {
|
|
1061
|
+
#syncInputAria(input, value, range) {
|
|
774
1062
|
if (input.getAttribute("role") !== "spinbutton") {
|
|
775
1063
|
this.#releaseInputAria(input);
|
|
776
1064
|
return;
|
|
777
1065
|
}
|
|
778
1066
|
this.#ariaValueNow.write(input, value === null ? null : String(value));
|
|
779
|
-
this.#ariaValueMin.write(input, Number.isFinite(
|
|
780
|
-
this.#ariaValueMax.write(input, Number.isFinite(
|
|
1067
|
+
this.#ariaValueMin.write(input, Number.isFinite(range.min) ? String(range.min) : null);
|
|
1068
|
+
this.#ariaValueMax.write(input, Number.isFinite(range.max) ? String(range.max) : null);
|
|
781
1069
|
}
|
|
782
|
-
/** Returns all custom-spinbutton ARIA leased on `input`. */
|
|
783
1070
|
#releaseInputAria(input) {
|
|
784
1071
|
this.#ariaValueNow.return(input);
|
|
785
1072
|
this.#ariaValueMin.return(input);
|
|
786
1073
|
this.#ariaValueMax.return(input);
|
|
787
1074
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
for (const button of this.incrementTargets)
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
#toggleButton(button, enabled) {
|
|
1075
|
+
#updateButtons(value, range) {
|
|
1076
|
+
const move = this.#move;
|
|
1077
|
+
for (const button of this.incrementTargets) {
|
|
1078
|
+
this.#toggleButton(button, value < range.max, move);
|
|
1079
|
+
if (!this.#moves.isLatest(move)) return;
|
|
1080
|
+
}
|
|
1081
|
+
for (const button of this.decrementTargets) {
|
|
1082
|
+
this.#toggleButton(button, value > range.min, move);
|
|
1083
|
+
if (!this.#moves.isLatest(move)) return;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
#toggleButton(button, enabled, move) {
|
|
800
1087
|
if (enabled) {
|
|
801
1088
|
if (button.hasAttribute(OWNED_DISABLED)) {
|
|
802
1089
|
button.disabled = false;
|
|
@@ -806,44 +1093,64 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
806
1093
|
}
|
|
807
1094
|
if (button.disabled) return;
|
|
808
1095
|
if (document.activeElement === button && this.hasInputTarget) this.inputTarget.focus();
|
|
1096
|
+
if (!this.#moves.isLatest(move)) return;
|
|
809
1097
|
button.disabled = true;
|
|
810
1098
|
button.setAttribute(OWNED_DISABLED, "");
|
|
811
1099
|
}
|
|
812
|
-
/** Returns `disabled` only when its explicit marker still identifies controller ownership. */
|
|
813
1100
|
#releaseButtonState(button) {
|
|
814
1101
|
if (!button.hasAttribute(OWNED_DISABLED)) return;
|
|
815
1102
|
button.disabled = false;
|
|
816
1103
|
button.removeAttribute(OWNED_DISABLED);
|
|
817
1104
|
}
|
|
818
|
-
/**
|
|
819
|
-
* The current numeric value, falling back to a finite min (else 0) when blank.
|
|
820
|
-
* Every read of the field's number comes through here, and the text is read
|
|
821
|
-
* through the shared half-width mapping: a text-type spinbutton holds whatever an
|
|
822
|
-
* IME confirmed, full-width digits and signs included (`34`, `-5`), while a
|
|
823
|
-
* native number field's value is already half-width or empty.
|
|
824
|
-
*/
|
|
825
1105
|
#currentValue() {
|
|
826
1106
|
const text = toHalfWidth(this.inputTarget.value);
|
|
827
1107
|
const parsed = Number(text);
|
|
828
1108
|
if (Number.isFinite(parsed) && text.trim() !== "") return parsed;
|
|
829
|
-
return Number.isFinite(this
|
|
1109
|
+
return Number.isFinite(this.#safeMin) ? this.#safeMin : 0;
|
|
830
1110
|
}
|
|
831
|
-
/** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */
|
|
832
1111
|
#normalize(raw) {
|
|
833
1112
|
return snapSteppedValue(raw, this.#steppedRange);
|
|
834
1113
|
}
|
|
835
|
-
/** Shared range configuration; finite endpoints remain allowed off the grid. */
|
|
836
1114
|
get #steppedRange() {
|
|
837
|
-
return { min: this
|
|
1115
|
+
return { min: this.#safeMin, max: this.#safeMax, step: this.#safeStep };
|
|
1116
|
+
}
|
|
1117
|
+
get #safeMin() {
|
|
1118
|
+
return this.#numbers.read(
|
|
1119
|
+
this,
|
|
1120
|
+
"min",
|
|
1121
|
+
this.minValue,
|
|
1122
|
+
_NumberInputController.values.min.default,
|
|
1123
|
+
_NumberInputController.valueConstraints.min
|
|
1124
|
+
);
|
|
838
1125
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1126
|
+
get #safeMax() {
|
|
1127
|
+
return this.#numbers.read(
|
|
1128
|
+
this,
|
|
1129
|
+
"max",
|
|
1130
|
+
this.maxValue,
|
|
1131
|
+
_NumberInputController.values.max.default,
|
|
1132
|
+
_NumberInputController.valueConstraints.max
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
get #safeStep() {
|
|
1136
|
+
return this.#numbers.read(
|
|
1137
|
+
this,
|
|
1138
|
+
"step",
|
|
1139
|
+
this.stepValue,
|
|
1140
|
+
_NumberInputController.values.step.default,
|
|
1141
|
+
_NumberInputController.valueConstraints.step
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
get #safePageStep() {
|
|
1145
|
+
return this.#numbers.read(
|
|
1146
|
+
this,
|
|
1147
|
+
"pageStep",
|
|
1148
|
+
this.pageStepValue,
|
|
1149
|
+
_NumberInputController.values.pageStep.default,
|
|
1150
|
+
_NumberInputController.valueConstraints.pageStep
|
|
1151
|
+
);
|
|
844
1152
|
}
|
|
845
1153
|
};
|
|
846
1154
|
|
|
847
1155
|
export { NumberInputController };
|
|
848
|
-
//# sourceMappingURL=number_input_controller.js.map
|
|
849
1156
|
//# sourceMappingURL=number_input_controller.js.map
|