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,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/character_counter_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/announce.ts
|
|
6
3
|
function announce(message, options = {}) {
|
|
7
4
|
const text = message.trim();
|
|
8
5
|
if (text.length === 0) return;
|
|
@@ -19,77 +16,153 @@ function fillTemplate(template, values) {
|
|
|
19
16
|
});
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
function sharedRegistry(key, create) {
|
|
20
|
+
const symbol = Symbol.for(key);
|
|
21
|
+
const scope = globalThis;
|
|
22
|
+
const existing = scope[symbol];
|
|
23
|
+
if (existing !== void 0) return existing;
|
|
24
|
+
const registry = create();
|
|
25
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
26
|
+
return registry;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isAttributeValue(value) {
|
|
30
|
+
return value === null || typeof value === "string";
|
|
31
|
+
}
|
|
32
|
+
function readLeaseRecord(element, record) {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
35
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
36
|
+
const [original, written, ...rest] = parsed;
|
|
37
|
+
const linked = rest[0] !== false;
|
|
38
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
39
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
40
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
41
|
+
} catch {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
var AttributeLease = class _AttributeLease {
|
|
46
|
+
static #registry = sharedRegistry(
|
|
47
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
48
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
49
|
+
);
|
|
24
50
|
#attribute;
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
51
|
+
#record;
|
|
52
|
+
#base;
|
|
53
|
+
#held = /* @__PURE__ */ new Set();
|
|
54
|
+
constructor(attribute, owner, options = {}) {
|
|
28
55
|
this.#attribute = attribute;
|
|
56
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
57
|
+
this.#base = options.base === true;
|
|
58
|
+
const names = _AttributeLease.#registry.names;
|
|
59
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
29
60
|
}
|
|
30
|
-
/** Writes or removes the leased attribute while preserving its authored value. */
|
|
31
61
|
write(element, value) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
this.#reflect(element, value);
|
|
62
|
+
const stack = this.#stack(element);
|
|
63
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
64
|
+
this.#held.add(element);
|
|
65
|
+
const shows = !this.#covered(element, stack, hold);
|
|
66
|
+
hold.written = value;
|
|
67
|
+
this.#mark(element, stack);
|
|
68
|
+
if (shows) this.#reflect(element, value);
|
|
42
69
|
}
|
|
43
|
-
/** Returns one lease without overwriting a value subsequently authored by a consumer. */
|
|
44
70
|
return(element) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
this.#
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
71
|
+
const held = this.#held.delete(element);
|
|
72
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
73
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
74
|
+
const hold = this.#take(stack);
|
|
75
|
+
if (!hold) return false;
|
|
76
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
77
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
78
|
+
const index = stack.indexOf(hold);
|
|
79
|
+
stack.splice(index, 1);
|
|
80
|
+
const above = stack[index];
|
|
81
|
+
if (above) above.linked &&= hold.linked;
|
|
82
|
+
this.#mark(element, stack);
|
|
83
|
+
if (!owns) return true;
|
|
84
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
85
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
returnAll() {
|
|
89
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
90
|
+
}
|
|
91
|
+
#stack(element) {
|
|
92
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
93
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
94
|
+
stacks.set(element, byAttribute);
|
|
95
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
96
|
+
byAttribute.set(this.#attribute, stack);
|
|
97
|
+
return stack;
|
|
98
|
+
}
|
|
99
|
+
#seed(element) {
|
|
100
|
+
const found = [];
|
|
101
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
102
|
+
const read = readLeaseRecord(element, record);
|
|
103
|
+
if (read) found.push({ ...read, record });
|
|
104
|
+
}
|
|
105
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
106
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
107
|
+
lease: null,
|
|
108
|
+
record,
|
|
109
|
+
written,
|
|
110
|
+
original,
|
|
111
|
+
linked: linked && index > 0
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
#take(stack) {
|
|
115
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
116
|
+
if (hold) hold.lease = this;
|
|
117
|
+
return hold;
|
|
118
|
+
}
|
|
119
|
+
#acquire(element, stack) {
|
|
120
|
+
const current = element.getAttribute(this.#attribute);
|
|
121
|
+
const top = stack.at(-1);
|
|
122
|
+
const bottom = stack[0];
|
|
123
|
+
const over = top?.written === current ? top : void 0;
|
|
124
|
+
const from = this.#base ? bottom : over;
|
|
125
|
+
const hold = {
|
|
126
|
+
lease: this,
|
|
127
|
+
record: this.#record,
|
|
128
|
+
written: current,
|
|
129
|
+
original: from ? from.original : current,
|
|
130
|
+
linked: !this.#base && over !== void 0
|
|
131
|
+
};
|
|
132
|
+
if (this.#base && bottom) {
|
|
133
|
+
bottom.linked = true;
|
|
134
|
+
stack.unshift(hold);
|
|
135
|
+
} else stack.push(hold);
|
|
136
|
+
return hold;
|
|
137
|
+
}
|
|
138
|
+
#covered(element, stack, hold) {
|
|
139
|
+
const current = element.getAttribute(this.#attribute);
|
|
140
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
141
|
+
}
|
|
142
|
+
#mark(element, stack) {
|
|
143
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
144
|
+
stack.forEach((hold, index) => {
|
|
145
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
146
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
147
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
148
|
+
records.set(
|
|
149
|
+
hold.record,
|
|
150
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
for (const [name, recorded] of records) {
|
|
154
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
155
|
+
if (recorded === null) element.removeAttribute(name);
|
|
156
|
+
else element.setAttribute(name, recorded);
|
|
157
|
+
}
|
|
50
158
|
}
|
|
51
|
-
/** Reflects only a real value transition, avoiding self-triggered mutation work. */
|
|
52
159
|
#reflect(element, value) {
|
|
53
160
|
if (element.getAttribute(this.#attribute) === value) return;
|
|
54
161
|
if (value === null) element.removeAttribute(this.#attribute);
|
|
55
162
|
else element.setAttribute(this.#attribute, value);
|
|
56
163
|
}
|
|
57
|
-
/** Returns every outstanding lease using the same ownership check as {@link return}. */
|
|
58
|
-
returnAll() {
|
|
59
|
-
for (const element of Array.from(this.#records.keys())) this.return(element);
|
|
60
|
-
}
|
|
61
164
|
};
|
|
62
165
|
|
|
63
|
-
// src/utils/before_cache_reset.ts
|
|
64
|
-
var BeforeCacheReset = class _BeforeCacheReset {
|
|
65
|
-
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
66
|
-
static #subscribers = /* @__PURE__ */ new Set();
|
|
67
|
-
/** The shared listener; installed while at least one instance is subscribed. */
|
|
68
|
-
static #onBeforeCache = () => {
|
|
69
|
-
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
70
|
-
};
|
|
71
|
-
#rewind;
|
|
72
|
-
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
73
|
-
constructor(rewind) {
|
|
74
|
-
this.#rewind = rewind;
|
|
75
|
-
}
|
|
76
|
-
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
77
|
-
activate() {
|
|
78
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
79
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
80
|
-
if (first) {
|
|
81
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
85
|
-
deactivate() {
|
|
86
|
-
_BeforeCacheReset.#subscribers.delete(this);
|
|
87
|
-
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
88
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// src/utils/composition_tracker.ts
|
|
93
166
|
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
94
167
|
"insertCompositionText",
|
|
95
168
|
"insertFromComposition",
|
|
@@ -101,13 +174,11 @@ var CompositionTracker = class {
|
|
|
101
174
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
102
175
|
#onStart;
|
|
103
176
|
#onEnd;
|
|
104
|
-
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
105
177
|
#confirmedTarget = null;
|
|
106
178
|
constructor(options = {}) {
|
|
107
179
|
this.#onStart = options.onStart;
|
|
108
180
|
this.#onEnd = options.onEnd;
|
|
109
181
|
}
|
|
110
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
111
182
|
observe(target) {
|
|
112
183
|
if (this.#observedTargets.has(target)) return;
|
|
113
184
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
@@ -115,7 +186,6 @@ var CompositionTracker = class {
|
|
|
115
186
|
target.addEventListener("keydown", this.#handleKeydown);
|
|
116
187
|
this.#observedTargets.add(target);
|
|
117
188
|
}
|
|
118
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
119
189
|
unobserve(target) {
|
|
120
190
|
if (!this.#observedTargets.delete(target)) return;
|
|
121
191
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
@@ -124,7 +194,6 @@ var CompositionTracker = class {
|
|
|
124
194
|
this.#activeTargets.delete(target);
|
|
125
195
|
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
126
196
|
}
|
|
127
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
128
197
|
disconnect() {
|
|
129
198
|
for (const target of this.#observedTargets) {
|
|
130
199
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
@@ -135,16 +204,9 @@ var CompositionTracker = class {
|
|
|
135
204
|
this.#activeTargets.clear();
|
|
136
205
|
this.#confirmedTarget = null;
|
|
137
206
|
}
|
|
138
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
139
207
|
isComposing(event) {
|
|
140
208
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
141
209
|
}
|
|
142
|
-
/**
|
|
143
|
-
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
144
|
-
*
|
|
145
|
-
* Asking closes the window either way, so one confirmation is folded at most
|
|
146
|
-
* once and a consumer asks once per `input`.
|
|
147
|
-
*/
|
|
148
210
|
consumesConfirmedInput(event) {
|
|
149
211
|
const confirmed = this.#confirmedTarget;
|
|
150
212
|
this.#confirmedTarget = null;
|
|
@@ -162,33 +224,38 @@ var CompositionTracker = class {
|
|
|
162
224
|
this.#confirmedTarget = event.target;
|
|
163
225
|
this.#onEnd?.(event);
|
|
164
226
|
};
|
|
165
|
-
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
166
227
|
#handleKeydown = () => {
|
|
167
228
|
this.#confirmedTarget = null;
|
|
168
229
|
};
|
|
169
230
|
};
|
|
170
231
|
|
|
171
|
-
|
|
232
|
+
var ListenerSet = class {
|
|
233
|
+
#abort = new AbortController();
|
|
234
|
+
add(target, type, handler, options) {
|
|
235
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
236
|
+
}
|
|
237
|
+
dispose() {
|
|
238
|
+
this.#abort.abort();
|
|
239
|
+
this.#abort = new AbortController();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
172
243
|
var MicrotaskCoalescer = class {
|
|
173
244
|
#run;
|
|
174
245
|
#queued = false;
|
|
175
246
|
#active = false;
|
|
176
247
|
#generation = 0;
|
|
177
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
178
248
|
constructor(run) {
|
|
179
249
|
this.#run = run;
|
|
180
250
|
}
|
|
181
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
182
251
|
activate() {
|
|
183
252
|
this.#active = true;
|
|
184
253
|
}
|
|
185
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
186
254
|
cancel() {
|
|
187
255
|
this.#active = false;
|
|
188
256
|
this.#queued = false;
|
|
189
257
|
this.#generation += 1;
|
|
190
258
|
}
|
|
191
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
192
259
|
schedule() {
|
|
193
260
|
if (!this.#active || this.#queued) return;
|
|
194
261
|
this.#queued = true;
|
|
@@ -201,43 +268,93 @@ var MicrotaskCoalescer = class {
|
|
|
201
268
|
}
|
|
202
269
|
};
|
|
203
270
|
|
|
204
|
-
|
|
271
|
+
var MorphRenderWatcher = class {
|
|
272
|
+
#listeners = new ListenerSet();
|
|
273
|
+
#pass;
|
|
274
|
+
#element = null;
|
|
275
|
+
#onMorph = () => this.#pass.schedule();
|
|
276
|
+
constructor(run) {
|
|
277
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
278
|
+
}
|
|
279
|
+
observe(element) {
|
|
280
|
+
if (this.#element === element) return;
|
|
281
|
+
this.disconnect();
|
|
282
|
+
this.#element = element;
|
|
283
|
+
this.#pass.activate();
|
|
284
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
285
|
+
}
|
|
286
|
+
schedule() {
|
|
287
|
+
this.#pass.schedule();
|
|
288
|
+
}
|
|
289
|
+
disconnect() {
|
|
290
|
+
this.#listeners.dispose();
|
|
291
|
+
this.#pass.cancel();
|
|
292
|
+
this.#element = null;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
var NUMBER_BOUNDS = {
|
|
297
|
+
nonNegative: { finite: true, min: 0 }};
|
|
298
|
+
function matchesNumberBounds(value, bounds) {
|
|
299
|
+
if (!Number.isFinite(value)) {
|
|
300
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
301
|
+
if (direction === null) return false;
|
|
302
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
303
|
+
}
|
|
304
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
305
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
306
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
307
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
308
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function readNumber(raw, fallback, bounds) {
|
|
313
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
var NumberValueReader = class {
|
|
317
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
318
|
+
read(owner, name, raw, fallback, bounds) {
|
|
319
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
320
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
321
|
+
this.#lastRejected.delete(name);
|
|
322
|
+
return resolved;
|
|
323
|
+
}
|
|
324
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
325
|
+
const literal = owner.element.getAttribute(attribute);
|
|
326
|
+
if (literal === null) {
|
|
327
|
+
this.#lastRejected.delete(name);
|
|
328
|
+
return resolved;
|
|
329
|
+
}
|
|
330
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
331
|
+
this.#lastRejected.set(name, literal);
|
|
332
|
+
console.warn(
|
|
333
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return resolved;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
205
340
|
var TimerRegistry = class {
|
|
206
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
207
341
|
ids = /* @__PURE__ */ new Set();
|
|
208
|
-
/**
|
|
209
|
-
* Cancels a single tracked timer.
|
|
210
|
-
*
|
|
211
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
212
|
-
* registry), so callers can clear defensively without guarding.
|
|
213
|
-
*/
|
|
214
342
|
clear(id) {
|
|
215
343
|
if (this.ids.delete(id)) {
|
|
216
344
|
this.cancel(id);
|
|
217
345
|
}
|
|
218
346
|
}
|
|
219
|
-
/**
|
|
220
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
221
|
-
* to guarantee no timer outlives the element.
|
|
222
|
-
*/
|
|
223
347
|
clearAll() {
|
|
224
348
|
for (const id of this.ids) {
|
|
225
349
|
this.cancel(id);
|
|
226
350
|
}
|
|
227
351
|
this.ids.clear();
|
|
228
352
|
}
|
|
229
|
-
/** Number of timers currently tracked (pending). */
|
|
230
353
|
get size() {
|
|
231
354
|
return this.ids.size;
|
|
232
355
|
}
|
|
233
356
|
};
|
|
234
357
|
var SafeTimeout = class extends TimerRegistry {
|
|
235
|
-
/**
|
|
236
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
237
|
-
*
|
|
238
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
239
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
240
|
-
*/
|
|
241
358
|
set(callback, delay) {
|
|
242
359
|
const id = this.schedule(() => {
|
|
243
360
|
this.ids.delete(id);
|
|
@@ -254,8 +371,8 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
254
371
|
}
|
|
255
372
|
};
|
|
256
373
|
|
|
257
|
-
// src/controllers/character_counter_controller.ts
|
|
258
374
|
var CharacterCounterController = class _CharacterCounterController extends Controller {
|
|
375
|
+
#numbers = new NumberValueReader();
|
|
259
376
|
static targets = ["input", "output"];
|
|
260
377
|
static values = {
|
|
261
378
|
max: { type: Number, default: 0 },
|
|
@@ -263,13 +380,18 @@ var CharacterCounterController = class _CharacterCounterController extends Contr
|
|
|
263
380
|
mode: { type: String, default: "remaining" },
|
|
264
381
|
announceText: { type: String, default: "" }
|
|
265
382
|
};
|
|
383
|
+
static valueConstraints = {
|
|
384
|
+
max: NUMBER_BOUNDS.nonNegative,
|
|
385
|
+
warnAt: NUMBER_BOUNDS.nonNegative
|
|
386
|
+
};
|
|
266
387
|
static events = ["change", "reconcile"];
|
|
267
|
-
/** Delay (ms) before one settled count is sent to the shared announcer. */
|
|
268
388
|
static #announceDelay = 200;
|
|
269
389
|
#timeouts = new SafeTimeout();
|
|
270
|
-
#ariaInvalid = new AttributeLease(
|
|
271
|
-
|
|
272
|
-
|
|
390
|
+
#ariaInvalid = new AttributeLease(
|
|
391
|
+
"aria-invalid",
|
|
392
|
+
this.identifier
|
|
393
|
+
);
|
|
394
|
+
#repaint = new MorphRenderWatcher(() => this.#reconcile(true));
|
|
273
395
|
#composition = new CompositionTracker({
|
|
274
396
|
onEnd: (event) => this.#commitFrom(event.currentTarget)
|
|
275
397
|
});
|
|
@@ -285,56 +407,42 @@ var CharacterCounterController = class _CharacterCounterController extends Contr
|
|
|
285
407
|
if (this.#composition.isComposing(event)) return;
|
|
286
408
|
this.#commit(field);
|
|
287
409
|
};
|
|
288
|
-
/** Reflects the current DOM state and opens the mutation-reconciliation window. */
|
|
289
410
|
connect() {
|
|
290
|
-
this.#repaint.
|
|
291
|
-
this.#beforeCache.activate();
|
|
411
|
+
this.#repaint.observe(this.element);
|
|
292
412
|
this.#reconcile(false);
|
|
293
413
|
}
|
|
294
|
-
/** Releases listeners, timers, borrowed ARIA, and controller-owned state hooks. */
|
|
295
414
|
disconnect() {
|
|
296
|
-
this.#repaint.
|
|
297
|
-
this.#beforeCache.deactivate();
|
|
415
|
+
this.#repaint.disconnect();
|
|
298
416
|
this.#cancelAnnouncement();
|
|
299
417
|
this.#bindField(null);
|
|
300
|
-
this.#composition.disconnect();
|
|
301
418
|
this.#clearStateHooks();
|
|
302
419
|
this.#lastLength = null;
|
|
303
420
|
this.#lastDetail = null;
|
|
304
421
|
}
|
|
305
|
-
/** Rebinds and repaints after an input target is added or replaced at runtime. */
|
|
306
422
|
inputTargetConnected() {
|
|
307
423
|
this.#repaint.schedule();
|
|
308
424
|
}
|
|
309
|
-
/** Releases and repaints after an input target is removed or replaced at runtime. */
|
|
310
425
|
inputTargetDisconnected() {
|
|
311
426
|
this.#repaint.schedule();
|
|
312
427
|
}
|
|
313
|
-
/** Initializes a display target inserted after the controller connected. */
|
|
314
428
|
outputTargetConnected() {
|
|
315
429
|
this.#repaint.schedule();
|
|
316
430
|
}
|
|
317
|
-
/** Reconciles the optional display target set after a removal or replacement. */
|
|
318
431
|
outputTargetDisconnected() {
|
|
319
432
|
this.#repaint.schedule();
|
|
320
433
|
}
|
|
321
|
-
/** Repaints when application code or a Turbo morph changes `max`. */
|
|
322
434
|
maxValueChanged() {
|
|
323
435
|
this.#repaint.schedule();
|
|
324
436
|
}
|
|
325
|
-
/** Repaints when application code or a Turbo morph changes `warnAt`. */
|
|
326
437
|
warnAtValueChanged() {
|
|
327
438
|
this.#repaint.schedule();
|
|
328
439
|
}
|
|
329
|
-
/** Repaints when application code or a Turbo morph changes `mode`. */
|
|
330
440
|
modeValueChanged() {
|
|
331
441
|
this.#repaint.schedule();
|
|
332
442
|
}
|
|
333
|
-
/** Rewords a pending announcement with the template the consumer changed it to. */
|
|
334
443
|
announceTextValueChanged() {
|
|
335
444
|
this.#repaint.schedule();
|
|
336
445
|
}
|
|
337
|
-
/** Rebinds one mutation batch and reports a changed controller-derived state. */
|
|
338
446
|
#reconcile(report) {
|
|
339
447
|
const previous = this.#lastDetail;
|
|
340
448
|
const owedField = this.#announceId === null ? null : this.#boundField;
|
|
@@ -357,13 +465,11 @@ var CharacterCounterController = class _CharacterCounterController extends Contr
|
|
|
357
465
|
this.dispatch("reconcile", { detail });
|
|
358
466
|
}
|
|
359
467
|
}
|
|
360
|
-
/** Commits a composition only when it came from the field still owned here. */
|
|
361
468
|
#commitFrom(target) {
|
|
362
469
|
const field = this.#boundField;
|
|
363
470
|
if (!field || target !== field || this.#field !== field) return;
|
|
364
471
|
this.#commit(field);
|
|
365
472
|
}
|
|
366
|
-
/** Reflects one confirmed user input and reports only a real length transition. */
|
|
367
473
|
#commit(field) {
|
|
368
474
|
const reading = this.#render(field);
|
|
369
475
|
const previousLength = this.#lastLength;
|
|
@@ -380,7 +486,6 @@ var CharacterCounterController = class _CharacterCounterController extends Contr
|
|
|
380
486
|
this.dispatch("change", { detail });
|
|
381
487
|
this.#scheduleAnnouncement(reading);
|
|
382
488
|
}
|
|
383
|
-
/** Selects the public event state from the richer internal reading. */
|
|
384
489
|
#detail(reading) {
|
|
385
490
|
return {
|
|
386
491
|
length: reading.length,
|
|
@@ -388,19 +493,13 @@ var CharacterCounterController = class _CharacterCounterController extends Contr
|
|
|
388
493
|
over: reading.over
|
|
389
494
|
};
|
|
390
495
|
}
|
|
391
|
-
/** Compares exactly the state carried by `change` and `reconcile`. */
|
|
392
496
|
#detailsDiffer(left, right) {
|
|
393
497
|
return left.length !== right.length || left.remaining !== right.remaining || left.over !== right.over;
|
|
394
498
|
}
|
|
395
|
-
/**
|
|
396
|
-
* Synchronizes visible count, state hooks, and temporary validation ARIA.
|
|
397
|
-
*
|
|
398
|
-
* @stimeoRenderRoot
|
|
399
|
-
*/
|
|
400
499
|
#render(field) {
|
|
401
500
|
const length = field.value.length;
|
|
402
|
-
const max = this.#normalizeCount(this
|
|
403
|
-
const warnAt = this.#normalizeCount(this
|
|
501
|
+
const max = this.#normalizeCount(this.#safeMax);
|
|
502
|
+
const warnAt = this.#normalizeCount(this.#safeWarnAt);
|
|
404
503
|
const remaining = max > 0 ? max - length : null;
|
|
405
504
|
const over = remaining !== null && remaining < 0;
|
|
406
505
|
const near = remaining !== null && warnAt > 0 && !over && remaining <= warnAt;
|
|
@@ -411,14 +510,11 @@ var CharacterCounterController = class _CharacterCounterController extends Contr
|
|
|
411
510
|
this.#writeOutput(text);
|
|
412
511
|
return { length, remaining, over, max, text };
|
|
413
512
|
}
|
|
414
|
-
/** Clears derived output when the declarative input set has no usable field. */
|
|
415
513
|
#renderEmpty() {
|
|
416
|
-
this.#ariaInvalid.returnAll();
|
|
417
514
|
this.#reflectedOver = null;
|
|
418
515
|
this.#clearStateHooks();
|
|
419
516
|
this.#writeOutput("");
|
|
420
517
|
}
|
|
421
|
-
/** Replaces the observed field symmetrically, returning state from the old one. */
|
|
422
518
|
#bindField(field) {
|
|
423
519
|
if (field === this.#boundField) return;
|
|
424
520
|
const previous = this.#boundField;
|
|
@@ -434,14 +530,12 @@ var CharacterCounterController = class _CharacterCounterController extends Contr
|
|
|
434
530
|
field.addEventListener("input", this.#onInput);
|
|
435
531
|
this.#composition.observe(field);
|
|
436
532
|
}
|
|
437
|
-
/** Leases `aria-invalid` only on the edge into over-limit, then returns it. */
|
|
438
533
|
#reflectInvalid(field, over) {
|
|
439
|
-
if (this.#reflectedOver === over) return;
|
|
534
|
+
if (this.#reflectedOver === over && (!over || field.hasAttribute("aria-invalid"))) return;
|
|
440
535
|
this.#reflectedOver = over;
|
|
441
536
|
if (over) this.#ariaInvalid.write(field, "true");
|
|
442
537
|
else this.#ariaInvalid.return(field);
|
|
443
538
|
}
|
|
444
|
-
/** Builds the visible count for the normalized display mode. */
|
|
445
539
|
#format(length, remaining, max) {
|
|
446
540
|
if (remaining === null) return String(length);
|
|
447
541
|
switch (this.#mode) {
|
|
@@ -453,12 +547,6 @@ var CharacterCounterController = class _CharacterCounterController extends Contr
|
|
|
453
547
|
return String(remaining);
|
|
454
548
|
}
|
|
455
549
|
}
|
|
456
|
-
/**
|
|
457
|
-
* Debounces one i18n-neutral message into the shared polite announcer.
|
|
458
|
-
*
|
|
459
|
-
* @stimeoRuntimeOnly `announceText` is the template of the one announcement this call schedules;
|
|
460
|
-
* the count is painted by the marked renderer.
|
|
461
|
-
*/
|
|
462
550
|
#scheduleAnnouncement(reading) {
|
|
463
551
|
this.#cancelAnnouncement();
|
|
464
552
|
const message = fillTemplate(this.announceTextValue, {
|
|
@@ -474,17 +562,14 @@ var CharacterCounterController = class _CharacterCounterController extends Contr
|
|
|
474
562
|
this.#announceId = null;
|
|
475
563
|
}, _CharacterCounterController.#announceDelay);
|
|
476
564
|
}
|
|
477
|
-
/** Cancels the one outstanding announcement without touching visible output. */
|
|
478
565
|
#cancelAnnouncement() {
|
|
479
566
|
if (this.#announceId !== null) this.#timeouts.clear(this.#announceId);
|
|
480
567
|
this.#announceId = null;
|
|
481
568
|
}
|
|
482
|
-
/** Writes the optional display only when its text actually changed. */
|
|
483
569
|
#writeOutput(text) {
|
|
484
570
|
if (!this.hasOutputTarget || this.outputTarget.textContent === text) return;
|
|
485
571
|
this.outputTarget.textContent = text;
|
|
486
572
|
}
|
|
487
|
-
/** Reflects a presence-style state hook without redundant attribute writes. */
|
|
488
573
|
#toggle(name, on) {
|
|
489
574
|
if (on) {
|
|
490
575
|
if (this.element.getAttribute(name) !== "true") this.element.setAttribute(name, "true");
|
|
@@ -492,29 +577,17 @@ var CharacterCounterController = class _CharacterCounterController extends Contr
|
|
|
492
577
|
this.element.removeAttribute(name);
|
|
493
578
|
}
|
|
494
579
|
}
|
|
495
|
-
/** Removes both state hooks owned by this controller. */
|
|
496
580
|
#clearStateHooks() {
|
|
497
581
|
this.#toggle("data-over-limit", false);
|
|
498
582
|
this.#toggle("data-near-limit", false);
|
|
499
583
|
}
|
|
500
|
-
/** Returns borrowed ARIA and transient hooks before Turbo snapshots the page. */
|
|
501
|
-
#rewindForCache() {
|
|
502
|
-
this.#cancelAnnouncement();
|
|
503
|
-
this.#ariaInvalid.returnAll();
|
|
504
|
-
this.#reflectedOver = null;
|
|
505
|
-
this.#clearStateHooks();
|
|
506
|
-
}
|
|
507
|
-
/** Converts a public count Value into a finite non-negative integer. */
|
|
508
584
|
#normalizeCount(raw) {
|
|
509
|
-
if (!Number.isFinite(raw)) return 0;
|
|
510
585
|
return Math.max(0, Math.trunc(raw));
|
|
511
586
|
}
|
|
512
|
-
/** Falls back to `remaining` when an authored display mode is unknown. */
|
|
513
587
|
get #mode() {
|
|
514
588
|
if (this.modeValue === "used" || this.modeValue === "both") return this.modeValue;
|
|
515
589
|
return "remaining";
|
|
516
590
|
}
|
|
517
|
-
/** The watched field: the first target, or a directly controlled form field. */
|
|
518
591
|
get #field() {
|
|
519
592
|
if (this.hasInputTarget) return this.inputTarget;
|
|
520
593
|
const element = this.element;
|
|
@@ -522,8 +595,25 @@ var CharacterCounterController = class _CharacterCounterController extends Contr
|
|
|
522
595
|
return element;
|
|
523
596
|
return null;
|
|
524
597
|
}
|
|
598
|
+
get #safeMax() {
|
|
599
|
+
return this.#numbers.read(
|
|
600
|
+
this,
|
|
601
|
+
"max",
|
|
602
|
+
this.maxValue,
|
|
603
|
+
_CharacterCounterController.values.max.default,
|
|
604
|
+
_CharacterCounterController.valueConstraints.max
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
get #safeWarnAt() {
|
|
608
|
+
return this.#numbers.read(
|
|
609
|
+
this,
|
|
610
|
+
"warnAt",
|
|
611
|
+
this.warnAtValue,
|
|
612
|
+
_CharacterCounterController.values.warnAt.default,
|
|
613
|
+
_CharacterCounterController.valueConstraints.warnAt
|
|
614
|
+
);
|
|
615
|
+
}
|
|
525
616
|
};
|
|
526
617
|
|
|
527
618
|
export { CharacterCounterController };
|
|
528
|
-
//# sourceMappingURL=character_counter_controller.js.map
|
|
529
619
|
//# sourceMappingURL=character_counter_controller.js.map
|