stimeo-ui 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,84 +1,157 @@
|
|
|
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
153
|
};
|
|
51
154
|
|
|
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
|
-
};
|
|
80
|
-
|
|
81
|
-
// src/utils/composition_tracker.ts
|
|
82
155
|
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
83
156
|
"insertCompositionText",
|
|
84
157
|
"insertFromComposition",
|
|
@@ -90,13 +163,11 @@ var CompositionTracker = class {
|
|
|
90
163
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
91
164
|
#onStart;
|
|
92
165
|
#onEnd;
|
|
93
|
-
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
94
166
|
#confirmedTarget = null;
|
|
95
167
|
constructor(options = {}) {
|
|
96
168
|
this.#onStart = options.onStart;
|
|
97
169
|
this.#onEnd = options.onEnd;
|
|
98
170
|
}
|
|
99
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
100
171
|
observe(target) {
|
|
101
172
|
if (this.#observedTargets.has(target)) return;
|
|
102
173
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
@@ -104,7 +175,6 @@ var CompositionTracker = class {
|
|
|
104
175
|
target.addEventListener("keydown", this.#handleKeydown);
|
|
105
176
|
this.#observedTargets.add(target);
|
|
106
177
|
}
|
|
107
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
108
178
|
unobserve(target) {
|
|
109
179
|
if (!this.#observedTargets.delete(target)) return;
|
|
110
180
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
@@ -113,7 +183,6 @@ var CompositionTracker = class {
|
|
|
113
183
|
this.#activeTargets.delete(target);
|
|
114
184
|
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
115
185
|
}
|
|
116
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
117
186
|
disconnect() {
|
|
118
187
|
for (const target of this.#observedTargets) {
|
|
119
188
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
@@ -124,16 +193,9 @@ var CompositionTracker = class {
|
|
|
124
193
|
this.#activeTargets.clear();
|
|
125
194
|
this.#confirmedTarget = null;
|
|
126
195
|
}
|
|
127
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
128
196
|
isComposing(event) {
|
|
129
197
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
130
198
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
133
|
-
*
|
|
134
|
-
* Asking closes the window either way, so one confirmation is folded at most
|
|
135
|
-
* once and a consumer asks once per `input`.
|
|
136
|
-
*/
|
|
137
199
|
consumesConfirmedInput(event) {
|
|
138
200
|
const confirmed = this.#confirmedTarget;
|
|
139
201
|
this.#confirmedTarget = null;
|
|
@@ -151,19 +213,15 @@ var CompositionTracker = class {
|
|
|
151
213
|
this.#confirmedTarget = event.target;
|
|
152
214
|
this.#onEnd?.(event);
|
|
153
215
|
};
|
|
154
|
-
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
155
216
|
#handleKeydown = () => {
|
|
156
217
|
this.#confirmedTarget = null;
|
|
157
218
|
};
|
|
158
219
|
};
|
|
159
220
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
control.dispatchEvent(new Event("input", { bubbles: true }));
|
|
163
|
-
control.dispatchEvent(new Event("change", { bubbles: true }));
|
|
221
|
+
function commitField(target) {
|
|
222
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
164
223
|
}
|
|
165
224
|
|
|
166
|
-
// src/utils/half_width.ts
|
|
167
225
|
var FULL_WIDTH_SHIFT = 65248;
|
|
168
226
|
function halfWidthChar(char) {
|
|
169
227
|
if (char >= "\uFF01" && char <= "\uFF5E") {
|
|
@@ -177,27 +235,33 @@ function toHalfWidth(text) {
|
|
|
177
235
|
return out;
|
|
178
236
|
}
|
|
179
237
|
|
|
180
|
-
|
|
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
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
181
249
|
var MicrotaskCoalescer = class {
|
|
182
250
|
#run;
|
|
183
251
|
#queued = false;
|
|
184
252
|
#active = false;
|
|
185
253
|
#generation = 0;
|
|
186
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
187
254
|
constructor(run) {
|
|
188
255
|
this.#run = run;
|
|
189
256
|
}
|
|
190
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
191
257
|
activate() {
|
|
192
258
|
this.#active = true;
|
|
193
259
|
}
|
|
194
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
195
260
|
cancel() {
|
|
196
261
|
this.#active = false;
|
|
197
262
|
this.#queued = false;
|
|
198
263
|
this.#generation += 1;
|
|
199
264
|
}
|
|
200
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
201
265
|
schedule() {
|
|
202
266
|
if (!this.#active || this.#queued) return;
|
|
203
267
|
this.#queued = true;
|
|
@@ -210,43 +274,107 @@ var MicrotaskCoalescer = class {
|
|
|
210
274
|
}
|
|
211
275
|
};
|
|
212
276
|
|
|
213
|
-
|
|
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
|
+
|
|
214
360
|
var TimerRegistry = class {
|
|
215
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
216
361
|
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
362
|
clear(id) {
|
|
224
363
|
if (this.ids.delete(id)) {
|
|
225
364
|
this.cancel(id);
|
|
226
365
|
}
|
|
227
366
|
}
|
|
228
|
-
/**
|
|
229
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
230
|
-
* to guarantee no timer outlives the element.
|
|
231
|
-
*/
|
|
232
367
|
clearAll() {
|
|
233
368
|
for (const id of this.ids) {
|
|
234
369
|
this.cancel(id);
|
|
235
370
|
}
|
|
236
371
|
this.ids.clear();
|
|
237
372
|
}
|
|
238
|
-
/** Number of timers currently tracked (pending). */
|
|
239
373
|
get size() {
|
|
240
374
|
return this.ids.size;
|
|
241
375
|
}
|
|
242
376
|
};
|
|
243
377
|
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
378
|
set(callback, delay) {
|
|
251
379
|
const id = this.schedule(() => {
|
|
252
380
|
this.ids.delete(id);
|
|
@@ -263,7 +391,6 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
263
391
|
}
|
|
264
392
|
};
|
|
265
393
|
var SafeInterval = class extends TimerRegistry {
|
|
266
|
-
/** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
|
|
267
394
|
set(callback, delay) {
|
|
268
395
|
const id = this.schedule(callback, delay);
|
|
269
396
|
this.ids.add(id);
|
|
@@ -277,7 +404,6 @@ var SafeInterval = class extends TimerRegistry {
|
|
|
277
404
|
}
|
|
278
405
|
};
|
|
279
406
|
|
|
280
|
-
// src/utils/stepped_value.ts
|
|
281
407
|
function effectiveStep(step) {
|
|
282
408
|
return Number.isFinite(step) && step > 0 ? step : 1;
|
|
283
409
|
}
|
|
@@ -383,9 +509,11 @@ function finiteFallback(min, max) {
|
|
|
383
509
|
return 0;
|
|
384
510
|
}
|
|
385
511
|
|
|
386
|
-
// src/controllers/number_input_controller.ts
|
|
387
512
|
var OWNED_DISABLED = "data-number-input-disabled";
|
|
388
513
|
var NumberInputController = class _NumberInputController extends Controller {
|
|
514
|
+
#numbers = new NumberValueReader();
|
|
515
|
+
#moves = new MoveCounter();
|
|
516
|
+
#move = 0;
|
|
389
517
|
static targets = ["input", "increment", "decrement"];
|
|
390
518
|
static values = {
|
|
391
519
|
min: { type: Number, default: Number.NEGATIVE_INFINITY },
|
|
@@ -393,70 +521,41 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
393
521
|
step: { type: Number, default: 1 },
|
|
394
522
|
pageStep: { type: Number, default: 0 }
|
|
395
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
|
+
};
|
|
396
530
|
static actions = ["decrement", "increment", "onInput", "onKeydown"];
|
|
397
531
|
static events = ["change", "reconcile"];
|
|
398
|
-
/** Delay (ms) a button must be held before auto-repeat starts. */
|
|
399
532
|
static #HOLD_DELAY_MS = 400;
|
|
400
|
-
/** Interval (ms) between auto-repeat steps once a hold has started. */
|
|
401
533
|
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
534
|
static #SUPPRESS_RESET_MS = 250;
|
|
409
|
-
/** Aborts global pointer guards on disconnect so none outlive the element. */
|
|
410
535
|
#globalGuards = null;
|
|
411
|
-
/** Per-target guards allow an old button to be released without touching its replacement. */
|
|
412
536
|
#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
537
|
#composition = new CompositionTracker({
|
|
418
538
|
onEnd: () => {
|
|
419
539
|
if (this.#reconcileHeld) this.#repaint.schedule();
|
|
420
540
|
}
|
|
421
541
|
});
|
|
422
|
-
/** A reconciliation that arrived while the input was composing. */
|
|
423
542
|
#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
|
-
*/
|
|
543
|
+
#ariaValueNow = new AttributeLease("aria-valuenow", this.identifier);
|
|
544
|
+
#ariaValueMin = new AttributeLease("aria-valuemin", this.identifier);
|
|
545
|
+
#ariaValueMax = new AttributeLease("aria-valuemax", this.identifier);
|
|
435
546
|
#lastValue = null;
|
|
436
|
-
/**
|
|
437
|
-
* Whether `connect()` has run and `disconnect()` has not since. The pass
|
|
438
|
-
* `connect()` runs takes the baseline without reporting it.
|
|
439
|
-
*/
|
|
440
547
|
#connected = false;
|
|
441
|
-
/** Timers for the hold delay and the suppress-reset safety net. */
|
|
442
548
|
#holdTimeouts = new SafeTimeout();
|
|
443
|
-
/** The running auto-repeat interval (one at a time). */
|
|
444
549
|
#holdIntervals = new SafeInterval();
|
|
445
|
-
/** True while a hold is armed/running, making `#stopHold` idempotent. */
|
|
446
550
|
#holdActive = false;
|
|
447
|
-
|
|
551
|
+
#holdGeneration = 0;
|
|
448
552
|
#repeatedDuringHold = false;
|
|
449
|
-
/** True when the next `click` is the trailing one after a hold and must be ignored. */
|
|
450
553
|
#suppressNextClick = false;
|
|
451
|
-
/** Button and pointer that own the current hold, so other instances/pointers cannot end it. */
|
|
452
554
|
#holdButton = null;
|
|
453
555
|
#holdPointerId = null;
|
|
454
|
-
|
|
455
|
-
#repaint = new MicrotaskCoalescer(() => this.#reconcile());
|
|
456
|
-
/** Normalizes any initial value and wires the focus/hold pointer guards. */
|
|
556
|
+
#repaint = new MorphRenderWatcher(() => this.#reconcile());
|
|
457
557
|
connect() {
|
|
458
|
-
this.#repaint.
|
|
459
|
-
this.#beforeCache.activate();
|
|
558
|
+
this.#repaint.observe(this.element);
|
|
460
559
|
this.#globalGuards = new AbortController();
|
|
461
560
|
const { signal } = this.#globalGuards;
|
|
462
561
|
for (const button of this.incrementTargets) this.#wireButton(button, 1);
|
|
@@ -467,17 +566,10 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
467
566
|
this.#reconcile();
|
|
468
567
|
this.#connected = true;
|
|
469
568
|
}
|
|
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
569
|
disconnect() {
|
|
477
570
|
this.#connected = false;
|
|
478
|
-
this.#repaint.
|
|
571
|
+
this.#repaint.disconnect();
|
|
479
572
|
this.#composition.disconnect();
|
|
480
|
-
this.#beforeCache.deactivate();
|
|
481
573
|
this.#globalGuards?.abort();
|
|
482
574
|
this.#globalGuards = null;
|
|
483
575
|
const buttons = /* @__PURE__ */ new Set([
|
|
@@ -492,74 +584,55 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
492
584
|
this.#repeatedDuringHold = false;
|
|
493
585
|
this.#suppressNextClick = false;
|
|
494
586
|
this.#holdTimeouts.clearAll();
|
|
495
|
-
this.#holdIntervals.clearAll();
|
|
496
587
|
this.#ariaValueNow.returnAll();
|
|
497
588
|
this.#ariaValueMin.returnAll();
|
|
498
589
|
this.#ariaValueMax.returnAll();
|
|
499
590
|
}
|
|
500
|
-
/** Reconciles the value against a minimum changed by application code or a Turbo morph. */
|
|
501
591
|
minValueChanged() {
|
|
502
592
|
this.#repaint.schedule();
|
|
503
593
|
}
|
|
504
|
-
/** Reconciles the value against a maximum changed by application code or a Turbo morph. */
|
|
505
594
|
maxValueChanged() {
|
|
506
595
|
this.#repaint.schedule();
|
|
507
596
|
}
|
|
508
|
-
/** Reconciles the value against a step changed by application code or a Turbo morph. */
|
|
509
597
|
stepValueChanged() {
|
|
510
598
|
this.#repaint.schedule();
|
|
511
599
|
}
|
|
512
|
-
/** Tracks and reconciles an input inserted or replaced at runtime. */
|
|
513
600
|
inputTargetConnected(input) {
|
|
514
601
|
this.#composition.observe(input);
|
|
515
602
|
this.#repaint.schedule();
|
|
516
603
|
}
|
|
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
604
|
inputTargetDisconnected(input) {
|
|
523
605
|
this.#composition.unobserve(input);
|
|
524
606
|
this.#releaseInputAria(input);
|
|
525
607
|
this.#stopHold(false);
|
|
526
608
|
this.#repaint.schedule();
|
|
527
609
|
}
|
|
528
|
-
/** Wires focus preservation and hold behavior on a runtime increment target. */
|
|
529
610
|
incrementTargetConnected(button) {
|
|
530
611
|
this.#wireButton(button, 1);
|
|
531
612
|
this.#repaint.schedule();
|
|
532
613
|
}
|
|
533
|
-
/** Releases only the increment target that actually disconnected. */
|
|
534
614
|
incrementTargetDisconnected(button) {
|
|
535
615
|
this.#unwireButton(button);
|
|
536
|
-
this.#repaint.schedule();
|
|
537
616
|
}
|
|
538
|
-
/** Wires focus preservation and hold behavior on a runtime decrement target. */
|
|
539
617
|
decrementTargetConnected(button) {
|
|
540
618
|
this.#wireButton(button, -1);
|
|
541
619
|
this.#repaint.schedule();
|
|
542
620
|
}
|
|
543
|
-
/** Releases only the decrement target that actually disconnected. */
|
|
544
621
|
decrementTargetDisconnected(button) {
|
|
545
622
|
this.#unwireButton(button);
|
|
546
|
-
this.#repaint.schedule();
|
|
547
623
|
}
|
|
548
|
-
/** Increases by one step. Bound via `data-action` (click). */
|
|
549
624
|
increment() {
|
|
550
625
|
if (this.#consumeSuppressedClick()) return;
|
|
551
626
|
if (!this.hasInputTarget) return;
|
|
552
627
|
this.#commitStep(1);
|
|
553
628
|
this.inputTarget.focus();
|
|
554
629
|
}
|
|
555
|
-
/** Decreases by one step. Bound via `data-action` (click). */
|
|
556
630
|
decrement() {
|
|
557
631
|
if (this.#consumeSuppressedClick()) return;
|
|
558
632
|
if (!this.hasInputTarget) return;
|
|
559
633
|
this.#commitStep(-1);
|
|
560
634
|
this.inputTarget.focus();
|
|
561
635
|
}
|
|
562
|
-
/** Clamps and snaps a typed value. Bound via `data-action` (change). */
|
|
563
636
|
onInput() {
|
|
564
637
|
if (!this.hasInputTarget) return;
|
|
565
638
|
if (this.inputTarget.value.trim() === "") {
|
|
@@ -569,7 +642,6 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
569
642
|
}
|
|
570
643
|
this.#commit(this.#currentValue());
|
|
571
644
|
}
|
|
572
|
-
/** Keyboard stepping per the APG spinbutton model. */
|
|
573
645
|
onKeydown(event) {
|
|
574
646
|
if (!this.hasInputTarget) return;
|
|
575
647
|
if (this.#composition.isComposing(event)) return;
|
|
@@ -583,18 +655,18 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
583
655
|
next = stepSteppedValue(this.#currentValue(), -1, this.#steppedRange);
|
|
584
656
|
break;
|
|
585
657
|
case "PageUp":
|
|
586
|
-
next = this
|
|
658
|
+
next = this.#safePageStep > 0 ? this.#currentValue() + this.#safePageStep : stepSteppedValue(this.#currentValue(), 10, this.#steppedRange);
|
|
587
659
|
break;
|
|
588
660
|
case "PageDown":
|
|
589
|
-
next = this
|
|
661
|
+
next = this.#safePageStep > 0 ? this.#currentValue() - this.#safePageStep : stepSteppedValue(this.#currentValue(), -10, this.#steppedRange);
|
|
590
662
|
break;
|
|
591
663
|
case "Home":
|
|
592
|
-
if (!Number.isFinite(this
|
|
593
|
-
next = this
|
|
664
|
+
if (!Number.isFinite(this.#safeMin)) return;
|
|
665
|
+
next = this.#safeMin;
|
|
594
666
|
break;
|
|
595
667
|
case "End":
|
|
596
|
-
if (!Number.isFinite(this
|
|
597
|
-
next = this
|
|
668
|
+
if (!Number.isFinite(this.#safeMax)) return;
|
|
669
|
+
next = this.#safeMax;
|
|
598
670
|
break;
|
|
599
671
|
default:
|
|
600
672
|
return;
|
|
@@ -602,11 +674,6 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
602
674
|
event.preventDefault();
|
|
603
675
|
this.#commit(next, true);
|
|
604
676
|
}
|
|
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
677
|
#wireButton(button, direction) {
|
|
611
678
|
if (this.#buttonGuards.has(button)) return;
|
|
612
679
|
const guard = new AbortController();
|
|
@@ -622,14 +689,12 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
622
689
|
{ signal: guard.signal }
|
|
623
690
|
);
|
|
624
691
|
}
|
|
625
|
-
/** Aborts one target's listeners and returns any state derived onto that button. */
|
|
626
692
|
#unwireButton(button) {
|
|
627
693
|
this.#buttonGuards.get(button)?.abort();
|
|
628
694
|
this.#buttonGuards.delete(button);
|
|
629
695
|
if (this.#holdButton === button) this.#stopHold(false);
|
|
630
696
|
this.#releaseButtonState(button);
|
|
631
697
|
}
|
|
632
|
-
/** Starts a hold: focus the input, then schedule the first repeat after a delay. */
|
|
633
698
|
#armHold(event, button, direction) {
|
|
634
699
|
const pointerButton = event.button;
|
|
635
700
|
if (typeof pointerButton === "number" && pointerButton !== 0) return;
|
|
@@ -642,25 +707,19 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
642
707
|
this.#holdPointerId = this.#pointerId(event);
|
|
643
708
|
this.#repeatedDuringHold = false;
|
|
644
709
|
this.#suppressNextClick = false;
|
|
710
|
+
const generation = ++this.#holdGeneration;
|
|
645
711
|
this.#holdTimeouts.set(() => {
|
|
646
|
-
if (!this.#commitStep(direction))
|
|
647
|
-
this.#stopHold();
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
712
|
+
if (!this.#commitStep(direction) || generation !== this.#holdGeneration) return;
|
|
650
713
|
this.#repeatedDuringHold = true;
|
|
651
714
|
this.#holdIntervals.set(() => {
|
|
652
715
|
if (!this.#commitStep(direction)) this.#stopHold();
|
|
653
716
|
}, _NumberInputController.#HOLD_REPEAT_MS);
|
|
654
717
|
}, _NumberInputController.#HOLD_DELAY_MS);
|
|
655
718
|
}
|
|
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
719
|
#stopHold(suppressTrailingClick = true) {
|
|
662
720
|
if (!this.#holdActive) return;
|
|
663
721
|
this.#holdActive = false;
|
|
722
|
+
this.#holdGeneration += 1;
|
|
664
723
|
this.#holdButton = null;
|
|
665
724
|
this.#holdPointerId = null;
|
|
666
725
|
this.#holdTimeouts.clearAll();
|
|
@@ -670,64 +729,50 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
670
729
|
this.#holdTimeouts.set(() => {
|
|
671
730
|
this.#suppressNextClick = false;
|
|
672
731
|
}, _NumberInputController.#SUPPRESS_RESET_MS);
|
|
673
|
-
} else if (!suppressTrailingClick) {
|
|
674
|
-
this.#suppressNextClick = false;
|
|
675
732
|
}
|
|
676
733
|
}
|
|
677
|
-
/** Ends a hold only for its initiating pointer (legacy/synthetic events remain accepted). */
|
|
678
734
|
#onPointerEnd = (event) => {
|
|
679
735
|
if (this.#ownsPointer(event)) this.#stopHold();
|
|
680
736
|
};
|
|
681
|
-
/** Window deactivation is global and always ends the active hold. */
|
|
682
737
|
#onWindowBlur = () => {
|
|
683
738
|
this.#stopHold();
|
|
684
739
|
};
|
|
685
|
-
/** Extracts pointer identity when the runtime event exposes it. */
|
|
686
740
|
#pointerId(event) {
|
|
687
741
|
return event.pointerId ?? null;
|
|
688
742
|
}
|
|
689
|
-
/** Whether `event` belongs to the active pointer, with a synthetic-event fallback. */
|
|
690
743
|
#ownsPointer(event) {
|
|
691
744
|
const pointerId = this.#pointerId(event);
|
|
692
745
|
return this.#holdActive && (this.#holdPointerId === null || pointerId === null || pointerId === this.#holdPointerId);
|
|
693
746
|
}
|
|
694
|
-
/** Consumes a pending trailing-click suppression; returns true if the click was swallowed. */
|
|
695
747
|
#consumeSuppressedClick() {
|
|
696
748
|
if (!this.#suppressNextClick) return false;
|
|
697
749
|
this.#suppressNextClick = false;
|
|
698
750
|
this.#holdTimeouts.clearAll();
|
|
699
751
|
return true;
|
|
700
752
|
}
|
|
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
753
|
#commit(raw, report = false) {
|
|
708
754
|
const value = this.#normalize(raw);
|
|
709
755
|
const changed = this.#lastValue === null || value !== this.#lastValue;
|
|
710
756
|
const shown = this.inputTarget.value;
|
|
711
|
-
this.#write(value);
|
|
712
757
|
this.#lastValue = value;
|
|
713
|
-
if (
|
|
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;
|
|
714
769
|
if (changed) this.dispatch("change", { detail: { value } });
|
|
715
770
|
return changed;
|
|
716
771
|
}
|
|
717
|
-
/** Commits an adjacent endpoint/grid value and reports whether it moved. */
|
|
718
772
|
#commitStep(count) {
|
|
719
773
|
if (!this.hasInputTarget) return false;
|
|
720
774
|
return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange), true);
|
|
721
775
|
}
|
|
722
|
-
/**
|
|
723
|
-
* Reflects the current value after a range, step or input change, reporting a
|
|
724
|
-
* number that moved from the one shown last as `reconcile`. While the input is
|
|
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
|
-
*/
|
|
731
776
|
#reconcile() {
|
|
732
777
|
if (!this.hasInputTarget) return;
|
|
733
778
|
if (this.#composition.isComposing()) {
|
|
@@ -738,65 +783,57 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
738
783
|
const previous = this.#lastValue;
|
|
739
784
|
if (this.inputTarget.value.trim() !== "") {
|
|
740
785
|
const value = this.#normalize(this.#currentValue());
|
|
741
|
-
this.#write(value);
|
|
742
786
|
this.#lastValue = value;
|
|
787
|
+
const move = this.#move;
|
|
788
|
+
this.#write(value);
|
|
789
|
+
if (!this.#moves.isLatest(move)) return;
|
|
743
790
|
} else {
|
|
744
791
|
this.#lastValue = null;
|
|
745
792
|
this.#reflectEmpty();
|
|
746
793
|
}
|
|
747
794
|
this.#reportMove(previous);
|
|
748
795
|
}
|
|
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
796
|
#reportMove(previous) {
|
|
756
797
|
const value = this.#lastValue;
|
|
757
798
|
if (!this.#connected || value === null || value === previous) return;
|
|
758
799
|
this.dispatch("reconcile", { detail: { value } });
|
|
759
800
|
}
|
|
760
|
-
/** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */
|
|
761
801
|
#write(value) {
|
|
762
802
|
const input = this.inputTarget;
|
|
763
|
-
input.value = String(value);
|
|
803
|
+
if (input.value !== String(value)) input.value = String(value);
|
|
764
804
|
this.#syncInputAria(input, value);
|
|
765
805
|
this.#updateButtons(value);
|
|
766
806
|
}
|
|
767
|
-
/** Reflects a blank value without inventing a numeric `aria-valuenow`. */
|
|
768
807
|
#reflectEmpty() {
|
|
769
808
|
this.#syncInputAria(this.inputTarget, null);
|
|
770
809
|
this.#updateButtons(this.#currentValue());
|
|
771
810
|
}
|
|
772
|
-
/** Synchronizes controller-derived ARIA while preserving authored teardown values. */
|
|
773
811
|
#syncInputAria(input, value) {
|
|
774
812
|
if (input.getAttribute("role") !== "spinbutton") {
|
|
775
813
|
this.#releaseInputAria(input);
|
|
776
814
|
return;
|
|
777
815
|
}
|
|
778
816
|
this.#ariaValueNow.write(input, value === null ? null : String(value));
|
|
779
|
-
this.#ariaValueMin.write(input, Number.isFinite(this
|
|
780
|
-
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);
|
|
781
819
|
}
|
|
782
|
-
/** Returns all custom-spinbutton ARIA leased on `input`. */
|
|
783
820
|
#releaseInputAria(input) {
|
|
784
821
|
this.#ariaValueNow.return(input);
|
|
785
822
|
this.#ariaValueMin.return(input);
|
|
786
823
|
this.#ariaValueMax.return(input);
|
|
787
824
|
}
|
|
788
|
-
/** Disables a step button at its bound, returning focus to the input first. */
|
|
789
825
|
#updateButtons(value) {
|
|
790
|
-
|
|
791
|
-
for (const button of this.
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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) {
|
|
800
837
|
if (enabled) {
|
|
801
838
|
if (button.hasAttribute(OWNED_DISABLED)) {
|
|
802
839
|
button.disabled = false;
|
|
@@ -806,44 +843,64 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
806
843
|
}
|
|
807
844
|
if (button.disabled) return;
|
|
808
845
|
if (document.activeElement === button && this.hasInputTarget) this.inputTarget.focus();
|
|
846
|
+
if (!this.#moves.isLatest(move)) return;
|
|
809
847
|
button.disabled = true;
|
|
810
848
|
button.setAttribute(OWNED_DISABLED, "");
|
|
811
849
|
}
|
|
812
|
-
/** Returns `disabled` only when its explicit marker still identifies controller ownership. */
|
|
813
850
|
#releaseButtonState(button) {
|
|
814
851
|
if (!button.hasAttribute(OWNED_DISABLED)) return;
|
|
815
852
|
button.disabled = false;
|
|
816
853
|
button.removeAttribute(OWNED_DISABLED);
|
|
817
854
|
}
|
|
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
855
|
#currentValue() {
|
|
826
856
|
const text = toHalfWidth(this.inputTarget.value);
|
|
827
857
|
const parsed = Number(text);
|
|
828
858
|
if (Number.isFinite(parsed) && text.trim() !== "") return parsed;
|
|
829
|
-
return Number.isFinite(this
|
|
859
|
+
return Number.isFinite(this.#safeMin) ? this.#safeMin : 0;
|
|
830
860
|
}
|
|
831
|
-
/** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */
|
|
832
861
|
#normalize(raw) {
|
|
833
862
|
return snapSteppedValue(raw, this.#steppedRange);
|
|
834
863
|
}
|
|
835
|
-
/** Shared range configuration; finite endpoints remain allowed off the grid. */
|
|
836
864
|
get #steppedRange() {
|
|
837
|
-
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
|
+
);
|
|
838
875
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
+
);
|
|
844
902
|
}
|
|
845
903
|
};
|
|
846
904
|
|
|
847
905
|
export { NumberInputController };
|
|
848
|
-
//# sourceMappingURL=number_input_controller.js.map
|
|
849
906
|
//# sourceMappingURL=number_input_controller.js.map
|