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,73 +1,223 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function sharedRegistry(key, create) {
|
|
4
|
+
const symbol = Symbol.for(key);
|
|
5
|
+
const scope = globalThis;
|
|
6
|
+
const existing = scope[symbol];
|
|
7
|
+
if (existing !== void 0) return existing;
|
|
8
|
+
const registry = create();
|
|
9
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
10
|
+
return registry;
|
|
11
|
+
}
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
function isAttributeValue(value) {
|
|
14
|
+
return value === null || typeof value === "string";
|
|
15
|
+
}
|
|
16
|
+
function readLeaseRecord(element, record) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
19
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
20
|
+
const [original, written, ...rest] = parsed;
|
|
21
|
+
const linked = rest[0] !== false;
|
|
22
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
23
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
24
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
25
|
+
} catch {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var AttributeLease = class _AttributeLease {
|
|
30
|
+
static #registry = sharedRegistry(
|
|
31
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
32
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
33
|
+
);
|
|
34
|
+
#attribute;
|
|
35
|
+
#record;
|
|
36
|
+
#base;
|
|
37
|
+
#held = /* @__PURE__ */ new Set();
|
|
38
|
+
constructor(attribute, owner, options = {}) {
|
|
39
|
+
this.#attribute = attribute;
|
|
40
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
41
|
+
this.#base = options.base === true;
|
|
42
|
+
const names = _AttributeLease.#registry.names;
|
|
43
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
44
|
+
}
|
|
45
|
+
write(element, value) {
|
|
46
|
+
const stack = this.#stack(element);
|
|
47
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
48
|
+
this.#held.add(element);
|
|
49
|
+
const shows = !this.#covered(element, stack, hold);
|
|
50
|
+
hold.written = value;
|
|
51
|
+
this.#mark(element, stack);
|
|
52
|
+
if (shows) this.#reflect(element, value);
|
|
53
|
+
}
|
|
54
|
+
return(element) {
|
|
55
|
+
const held = this.#held.delete(element);
|
|
56
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
57
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
58
|
+
const hold = this.#take(stack);
|
|
59
|
+
if (!hold) return false;
|
|
60
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
61
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
62
|
+
const index = stack.indexOf(hold);
|
|
63
|
+
stack.splice(index, 1);
|
|
64
|
+
const above = stack[index];
|
|
65
|
+
if (above) above.linked &&= hold.linked;
|
|
66
|
+
this.#mark(element, stack);
|
|
67
|
+
if (!owns) return true;
|
|
68
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
69
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
returnAll() {
|
|
73
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
74
|
+
}
|
|
75
|
+
#stack(element) {
|
|
76
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
77
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
78
|
+
stacks.set(element, byAttribute);
|
|
79
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
80
|
+
byAttribute.set(this.#attribute, stack);
|
|
81
|
+
return stack;
|
|
82
|
+
}
|
|
83
|
+
#seed(element) {
|
|
84
|
+
const found = [];
|
|
85
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
86
|
+
const read = readLeaseRecord(element, record);
|
|
87
|
+
if (read) found.push({ ...read, record });
|
|
24
88
|
}
|
|
89
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
90
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
91
|
+
lease: null,
|
|
92
|
+
record,
|
|
93
|
+
written,
|
|
94
|
+
original,
|
|
95
|
+
linked: linked && index > 0
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
#take(stack) {
|
|
99
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
100
|
+
if (hold) hold.lease = this;
|
|
101
|
+
return hold;
|
|
102
|
+
}
|
|
103
|
+
#acquire(element, stack) {
|
|
104
|
+
const current = element.getAttribute(this.#attribute);
|
|
105
|
+
const top = stack.at(-1);
|
|
106
|
+
const bottom = stack[0];
|
|
107
|
+
const over = top?.written === current ? top : void 0;
|
|
108
|
+
const from = this.#base ? bottom : over;
|
|
109
|
+
const hold = {
|
|
110
|
+
lease: this,
|
|
111
|
+
record: this.#record,
|
|
112
|
+
written: current,
|
|
113
|
+
original: from ? from.original : current,
|
|
114
|
+
linked: !this.#base && over !== void 0
|
|
115
|
+
};
|
|
116
|
+
if (this.#base && bottom) {
|
|
117
|
+
bottom.linked = true;
|
|
118
|
+
stack.unshift(hold);
|
|
119
|
+
} else stack.push(hold);
|
|
120
|
+
return hold;
|
|
121
|
+
}
|
|
122
|
+
#covered(element, stack, hold) {
|
|
123
|
+
const current = element.getAttribute(this.#attribute);
|
|
124
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
125
|
+
}
|
|
126
|
+
#mark(element, stack) {
|
|
127
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
128
|
+
stack.forEach((hold, index) => {
|
|
129
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
130
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
131
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
132
|
+
records.set(
|
|
133
|
+
hold.record,
|
|
134
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
for (const [name, recorded] of records) {
|
|
138
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
139
|
+
if (recorded === null) element.removeAttribute(name);
|
|
140
|
+
else element.setAttribute(name, recorded);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
#reflect(element, value) {
|
|
144
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
145
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
146
|
+
else element.setAttribute(this.#attribute, value);
|
|
25
147
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
var ListenerSet = class {
|
|
151
|
+
#abort = new AbortController();
|
|
152
|
+
add(target, type, handler, options) {
|
|
153
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
154
|
+
}
|
|
155
|
+
dispose() {
|
|
156
|
+
this.#abort.abort();
|
|
157
|
+
this.#abort = new AbortController();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
function matchesNumberBounds(value, bounds) {
|
|
162
|
+
if (!Number.isFinite(value)) {
|
|
163
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
164
|
+
if (direction === null) return false;
|
|
165
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
166
|
+
}
|
|
167
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
168
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
169
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
170
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
171
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readNumber(raw, fallback, bounds) {
|
|
176
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
var NumberValueReader = class {
|
|
180
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
181
|
+
read(owner, name, raw, fallback, bounds) {
|
|
182
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
183
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
184
|
+
this.#lastRejected.delete(name);
|
|
185
|
+
return resolved;
|
|
186
|
+
}
|
|
187
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
188
|
+
const literal = owner.element.getAttribute(attribute);
|
|
189
|
+
if (literal === null) {
|
|
190
|
+
this.#lastRejected.delete(name);
|
|
191
|
+
return resolved;
|
|
192
|
+
}
|
|
193
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
194
|
+
this.#lastRejected.set(name, literal);
|
|
195
|
+
console.warn(
|
|
196
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return resolved;
|
|
31
200
|
}
|
|
32
201
|
};
|
|
33
202
|
|
|
34
|
-
// src/utils/safe_timeout.ts
|
|
35
203
|
var TimerRegistry = class {
|
|
36
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
37
204
|
ids = /* @__PURE__ */ new Set();
|
|
38
|
-
/**
|
|
39
|
-
* Cancels a single tracked timer.
|
|
40
|
-
*
|
|
41
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
42
|
-
* registry), so callers can clear defensively without guarding.
|
|
43
|
-
*/
|
|
44
205
|
clear(id) {
|
|
45
206
|
if (this.ids.delete(id)) {
|
|
46
207
|
this.cancel(id);
|
|
47
208
|
}
|
|
48
209
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
51
|
-
* to guarantee no timer outlives the element.
|
|
52
|
-
*/
|
|
53
210
|
clearAll() {
|
|
54
211
|
for (const id of this.ids) {
|
|
55
212
|
this.cancel(id);
|
|
56
213
|
}
|
|
57
214
|
this.ids.clear();
|
|
58
215
|
}
|
|
59
|
-
/** Number of timers currently tracked (pending). */
|
|
60
216
|
get size() {
|
|
61
217
|
return this.ids.size;
|
|
62
218
|
}
|
|
63
219
|
};
|
|
64
220
|
var SafeTimeout = class extends TimerRegistry {
|
|
65
|
-
/**
|
|
66
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
67
|
-
*
|
|
68
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
69
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
70
|
-
*/
|
|
71
221
|
set(callback, delay) {
|
|
72
222
|
const id = this.schedule(() => {
|
|
73
223
|
this.ids.delete(id);
|
|
@@ -84,16 +234,20 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
84
234
|
}
|
|
85
235
|
};
|
|
86
236
|
|
|
87
|
-
|
|
88
|
-
|
|
237
|
+
var StateRegions = class _StateRegions {
|
|
238
|
+
static #registry = sharedRegistry(
|
|
239
|
+
"stimeo-ui.state-regions.registry.v1",
|
|
240
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
241
|
+
);
|
|
89
242
|
#whenTrue;
|
|
90
243
|
#whenFalse;
|
|
91
|
-
#
|
|
92
|
-
constructor(options) {
|
|
244
|
+
#record;
|
|
245
|
+
constructor(options, owner) {
|
|
93
246
|
this.#whenTrue = options.whenTrue;
|
|
94
247
|
this.#whenFalse = options.whenFalse ?? null;
|
|
248
|
+
this.#record = `data-${owner}-hidden-region`;
|
|
249
|
+
_StateRegions.#registry.names.add(this.#record);
|
|
95
250
|
}
|
|
96
|
-
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
97
251
|
reflect(host, isTrue) {
|
|
98
252
|
const shown = this.#inside(host, this.#whenTrue());
|
|
99
253
|
if (!this.#whenFalse) {
|
|
@@ -109,111 +263,232 @@ var StateRegions = class {
|
|
|
109
263
|
for (const region of shown) this.#write(region, !isTrue);
|
|
110
264
|
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
111
265
|
}
|
|
112
|
-
|
|
266
|
+
release(host) {
|
|
267
|
+
for (const region of [host, ...host.querySelectorAll("*")]) {
|
|
268
|
+
if (!(region instanceof HTMLElement)) continue;
|
|
269
|
+
const stack = this.#held(region);
|
|
270
|
+
const hold = stack && this.#take(stack);
|
|
271
|
+
if (!stack || !hold) continue;
|
|
272
|
+
const owns = region.hidden === hold.written && !this.#covered(region, stack, hold);
|
|
273
|
+
const index = stack.indexOf(hold);
|
|
274
|
+
stack.splice(index, 1);
|
|
275
|
+
const above = stack[index];
|
|
276
|
+
if (above) above.linked &&= hold.linked;
|
|
277
|
+
if (owns) {
|
|
278
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
279
|
+
if (below) {
|
|
280
|
+
if (region.hidden !== below.written) region.hidden = below.written;
|
|
281
|
+
} else if (region.getAttribute("hidden") !== hold.authored) {
|
|
282
|
+
if (hold.authored === null) region.removeAttribute("hidden");
|
|
283
|
+
else region.setAttribute("hidden", hold.authored);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
this.#mark(region, stack);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
113
289
|
#write(region, hidden) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
290
|
+
const stack = this.#stack(region);
|
|
291
|
+
const hold = this.#take(stack) ?? this.#acquire(region, stack);
|
|
292
|
+
const shows = !this.#covered(region, stack, hold);
|
|
293
|
+
hold.written = hidden;
|
|
294
|
+
if (shows && region.hidden !== hidden) region.hidden = hidden;
|
|
295
|
+
this.#mark(region, stack);
|
|
117
296
|
}
|
|
118
|
-
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
119
297
|
#give(region) {
|
|
120
|
-
|
|
298
|
+
const stack = this.#held(region);
|
|
299
|
+
if (stack && this.#take(stack)?.written) this.#write(region, false);
|
|
300
|
+
}
|
|
301
|
+
#held(region) {
|
|
302
|
+
return _StateRegions.#registry.stacks.has(region) || region.hasAttribute(this.#record) ? this.#stack(region) : void 0;
|
|
303
|
+
}
|
|
304
|
+
#stack(region) {
|
|
305
|
+
const stacks = _StateRegions.#registry.stacks;
|
|
306
|
+
const stack = stacks.get(region) ?? this.#seed(region);
|
|
307
|
+
stacks.set(region, stack);
|
|
308
|
+
return stack;
|
|
309
|
+
}
|
|
310
|
+
#seed(region) {
|
|
311
|
+
const found = [];
|
|
312
|
+
for (const record of _StateRegions.#registry.names) {
|
|
313
|
+
const read = this.#recorded(region, record);
|
|
314
|
+
if (read) found.push({ ...read, record });
|
|
315
|
+
}
|
|
316
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
317
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, authored, written, linked }, index) => ({
|
|
318
|
+
instance: null,
|
|
319
|
+
record,
|
|
320
|
+
authored,
|
|
321
|
+
written,
|
|
322
|
+
linked: linked && index > 0
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
#take(stack) {
|
|
326
|
+
const hold = stack.find((held) => held.instance === this) ?? stack.find((held) => held.instance === null && held.record === this.#record);
|
|
327
|
+
if (hold) hold.instance = this;
|
|
328
|
+
return hold;
|
|
329
|
+
}
|
|
330
|
+
#covered(region, stack, hold) {
|
|
331
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === region.hidden);
|
|
332
|
+
}
|
|
333
|
+
#acquire(region, stack) {
|
|
334
|
+
const top = stack.at(-1);
|
|
335
|
+
const over = top && region.hidden === top.written ? top : void 0;
|
|
336
|
+
const hold = {
|
|
337
|
+
instance: this,
|
|
338
|
+
record: this.#record,
|
|
339
|
+
authored: over ? over.authored : region.getAttribute("hidden"),
|
|
340
|
+
written: false,
|
|
341
|
+
linked: over !== void 0
|
|
342
|
+
};
|
|
343
|
+
stack.push(hold);
|
|
344
|
+
return hold;
|
|
345
|
+
}
|
|
346
|
+
#mark(region, stack) {
|
|
347
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
348
|
+
stack.forEach((hold, index) => {
|
|
349
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
350
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
351
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
352
|
+
records.set(
|
|
353
|
+
hold.record,
|
|
354
|
+
region.getAttribute("hidden") === hold.authored && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.authored, hold.written, ...marker, ...beneath])
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
for (const [name, recorded] of records) {
|
|
358
|
+
if (region.getAttribute(name) === recorded) continue;
|
|
359
|
+
if (recorded === null) region.removeAttribute(name);
|
|
360
|
+
else region.setAttribute(name, recorded);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
#recorded(region, name) {
|
|
364
|
+
try {
|
|
365
|
+
const parsed = JSON.parse(region.getAttribute(name) ?? "null");
|
|
366
|
+
if (!Array.isArray(parsed)) return null;
|
|
367
|
+
const [authored, written, ...rest] = parsed;
|
|
368
|
+
if (authored !== null && typeof authored !== "string") return null;
|
|
369
|
+
const linked = rest[0] !== false;
|
|
370
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
371
|
+
if (!beneath.every((under) => typeof under === "string")) return null;
|
|
372
|
+
return typeof written === "boolean" ? { authored, written, linked, beneath } : null;
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
121
376
|
}
|
|
122
|
-
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
123
377
|
#inside(host, regions) {
|
|
124
378
|
return regions.filter((region) => host.contains(region));
|
|
125
379
|
}
|
|
126
380
|
};
|
|
127
381
|
|
|
128
|
-
// src/controllers/password_reveal_controller.ts
|
|
129
382
|
var MAX_DELAY = 2 ** 31 - 1;
|
|
130
|
-
|
|
383
|
+
function maskCopyOf(type, input) {
|
|
384
|
+
const revealed = input.type === "text";
|
|
385
|
+
type.return(input);
|
|
386
|
+
if (revealed && input.type === "password") input.value = "";
|
|
387
|
+
}
|
|
388
|
+
var PasswordRevealController = class _PasswordRevealController extends Controller {
|
|
389
|
+
#numbers = new NumberValueReader();
|
|
131
390
|
static targets = ["input", "toggle", "onLabel", "offLabel"];
|
|
132
391
|
static values = {
|
|
133
392
|
autoHide: { type: Number, default: 0 }
|
|
134
393
|
};
|
|
394
|
+
static valueConstraints = {
|
|
395
|
+
autoHide: { finite: true, allowInfinity: "positive" }
|
|
396
|
+
};
|
|
135
397
|
static actions = ["toggle"];
|
|
136
398
|
static events = ["toggle"];
|
|
137
|
-
|
|
399
|
+
static #maskedRenders = sharedRegistry(
|
|
400
|
+
"stimeo-ui.password-reveal-render-mask.registry.v1",
|
|
401
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
402
|
+
);
|
|
138
403
|
#timers = new SafeTimeout();
|
|
139
|
-
/**
|
|
140
|
-
* Whether this controller is between `connect()` and `disconnect()`.
|
|
141
|
-
*
|
|
142
|
-
* Target callbacks outlive the controller: Stimulus stops the target observer
|
|
143
|
-
* after `disconnect()`, so a field leaving after teardown still reaches
|
|
144
|
-
* {@link PasswordRevealController.inputTargetDisconnected}. Arming from there
|
|
145
|
-
* would put a timer back that nothing will clear. The element staying in the
|
|
146
|
-
* document does not answer this — unloading the controller leaves it there.
|
|
147
|
-
*/
|
|
148
404
|
#connected = false;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
405
|
+
#labels = new StateRegions(
|
|
406
|
+
{
|
|
407
|
+
whenTrue: () => this.onLabelTargets,
|
|
408
|
+
whenFalse: () => this.offLabelTargets
|
|
409
|
+
},
|
|
410
|
+
this.identifier
|
|
411
|
+
);
|
|
412
|
+
#pressed = new AttributeLease("aria-pressed", this.identifier);
|
|
413
|
+
#type = new AttributeLease("type", this.identifier);
|
|
414
|
+
#listeners = new ListenerSet();
|
|
415
|
+
#lived = false;
|
|
156
416
|
connect() {
|
|
157
417
|
this.#connected = true;
|
|
418
|
+
if (!this.#lived) this.#maskCopy();
|
|
419
|
+
this.#lived = true;
|
|
158
420
|
const visible = this.#isVisible;
|
|
159
421
|
this.#reflect(visible);
|
|
160
|
-
this.#
|
|
422
|
+
this.#listeners.add(document, "turbo:before-cache", this.#rewindToMasked);
|
|
423
|
+
_PasswordRevealController.#maskRendersInto(this.element.ownerDocument, this.identifier);
|
|
161
424
|
this.#arm(visible);
|
|
162
425
|
}
|
|
163
426
|
disconnect() {
|
|
164
427
|
this.#connected = false;
|
|
165
428
|
this.#timers.clearAll();
|
|
166
|
-
this.#
|
|
429
|
+
this.#listeners.dispose();
|
|
167
430
|
}
|
|
168
|
-
/** Re-derives the hooks and the re-mask for a field swapped in after connect. */
|
|
169
431
|
inputTargetConnected() {
|
|
432
|
+
for (const input of this.inputTargets.slice(1)) this.#type.return(input);
|
|
170
433
|
const visible = this.#connected && this.#isVisible;
|
|
171
434
|
this.#reflect(visible);
|
|
172
435
|
this.#arm(visible);
|
|
173
436
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
* showing the password, and would carry no re-mask.
|
|
179
|
-
*
|
|
180
|
-
* Teardown is the one case with nothing to derive: target callbacks run after
|
|
181
|
-
* `disconnect()`, so the field is still revealed and re-arming from it would
|
|
182
|
-
* outlive the controller. A detached root is the signal to stand down.
|
|
183
|
-
*/
|
|
184
|
-
inputTargetDisconnected() {
|
|
185
|
-
const visible = this.#connected && this.element.isConnected && this.#isVisible;
|
|
437
|
+
inputTargetDisconnected(input) {
|
|
438
|
+
if (!this.inputTargets.includes(input)) this.#type.return(input);
|
|
439
|
+
if (!this.#connected) return;
|
|
440
|
+
const visible = this.#isVisible;
|
|
186
441
|
this.#reflect(visible);
|
|
187
442
|
this.#arm(visible);
|
|
188
443
|
}
|
|
189
|
-
/** Re-derives the pressed state for a button swapped in after connect. */
|
|
190
444
|
toggleTargetConnected() {
|
|
191
445
|
this.#reflect(this.#isVisible);
|
|
192
446
|
}
|
|
193
|
-
|
|
447
|
+
toggleTargetDisconnected(toggle) {
|
|
448
|
+
if (!this.toggleTargets.includes(toggle)) {
|
|
449
|
+
this.#pressed.return(toggle);
|
|
450
|
+
this.#labels.release(toggle);
|
|
451
|
+
}
|
|
452
|
+
if (this.#connected) this.#reflect(this.#isVisible);
|
|
453
|
+
}
|
|
194
454
|
onLabelTargetConnected() {
|
|
195
455
|
this.#reflect(this.#isVisible);
|
|
196
456
|
}
|
|
197
|
-
/** Describes the state to a masked-side label that arrives after connect. */
|
|
198
457
|
offLabelTargetConnected() {
|
|
199
458
|
this.#reflect(this.#isVisible);
|
|
200
459
|
}
|
|
201
|
-
/** Toggles the input between masked and revealed. Bound via `data-action`. */
|
|
202
460
|
toggle() {
|
|
203
461
|
this.#setVisible(!this.#isVisible);
|
|
204
462
|
}
|
|
205
|
-
/** Whether the input is currently revealed (`type="text"`). */
|
|
206
463
|
get #isVisible() {
|
|
207
464
|
return this.hasInputTarget && this.inputTarget.type === "text";
|
|
208
465
|
}
|
|
209
|
-
|
|
466
|
+
#maskCopy() {
|
|
467
|
+
if (this.hasInputTarget) maskCopyOf(this.#type, this.inputTarget);
|
|
468
|
+
}
|
|
469
|
+
static #maskRendersInto(document2, identifier) {
|
|
470
|
+
const masked = _PasswordRevealController.#maskedRenders.get(document2) ?? /* @__PURE__ */ new Set();
|
|
471
|
+
if (masked.has(identifier)) return;
|
|
472
|
+
masked.add(identifier);
|
|
473
|
+
_PasswordRevealController.#maskedRenders.set(document2, masked);
|
|
474
|
+
const type = new AttributeLease("type", identifier);
|
|
475
|
+
document2.addEventListener("turbo:before-render", (event) => {
|
|
476
|
+
const body = event.detail?.newBody;
|
|
477
|
+
if (!(body instanceof Element)) return;
|
|
478
|
+
for (const input of body.querySelectorAll(
|
|
479
|
+
`input[data-${identifier}-type-lease]`
|
|
480
|
+
)) {
|
|
481
|
+
if (input.type === "text") maskCopyOf(type, input);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
210
485
|
#setVisible(visible) {
|
|
211
486
|
if (!this.hasInputTarget) return;
|
|
212
487
|
const input = this.inputTarget;
|
|
213
488
|
const restoreInputFocus = document.activeElement === input;
|
|
214
489
|
const selectionStart = input.selectionStart;
|
|
215
490
|
const selectionEnd = input.selectionEnd;
|
|
216
|
-
input
|
|
491
|
+
this.#type.write(input, visible ? "text" : "password");
|
|
217
492
|
if (restoreInputFocus) {
|
|
218
493
|
input.focus();
|
|
219
494
|
if (selectionStart !== null && selectionEnd !== null) {
|
|
@@ -227,7 +502,6 @@ var PasswordRevealController = class extends Controller {
|
|
|
227
502
|
this.dispatch("toggle", { detail: { visible } });
|
|
228
503
|
this.#arm(visible);
|
|
229
504
|
}
|
|
230
|
-
/** Schedules the auto re-mask for a revealed field, replacing any pending one. */
|
|
231
505
|
#arm(visible) {
|
|
232
506
|
this.#timers.clearAll();
|
|
233
507
|
const delay = this.#autoHideDelay;
|
|
@@ -235,39 +509,33 @@ var PasswordRevealController = class extends Controller {
|
|
|
235
509
|
this.#timers.set(() => this.#setVisible(false), delay);
|
|
236
510
|
}
|
|
237
511
|
}
|
|
238
|
-
/**
|
|
239
|
-
* The auto re-mask delay, held to what `setTimeout` can carry. Past that limit
|
|
240
|
-
* a delay folds to zero, turning "keep it showing" into "hide it at once" —
|
|
241
|
-
* the opposite of what the declaration asked for. A value that is no delay at
|
|
242
|
-
* all stays out of the positive range {@link PasswordRevealController.#arm}
|
|
243
|
-
* requires, so it schedules nothing.
|
|
244
|
-
*
|
|
245
|
-
* @stimeoRuntimeOnly `autoHide` is the delay of the one re-masking timer it arms.
|
|
246
|
-
*/
|
|
247
512
|
get #autoHideDelay() {
|
|
248
|
-
return Math.min(this
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
* Returns the field to masked before Turbo copies the page. Silent and
|
|
252
|
-
* focus-free: the page is about to be frozen, so there is no one to tell and
|
|
253
|
-
* nowhere for focus to go.
|
|
254
|
-
*/
|
|
255
|
-
#rewindToMasked() {
|
|
513
|
+
return Math.min(this.#safeAutoHide, MAX_DELAY);
|
|
514
|
+
}
|
|
515
|
+
#rewindToMasked = () => {
|
|
256
516
|
this.#timers.clearAll();
|
|
257
517
|
if (!this.hasInputTarget) return;
|
|
258
|
-
this.inputTarget
|
|
518
|
+
this.#type.write(this.inputTarget, "password");
|
|
259
519
|
this.#reflect(false);
|
|
260
|
-
}
|
|
261
|
-
/** Reflects the visible state onto `aria-pressed`, `data-state` and the labels. */
|
|
520
|
+
};
|
|
262
521
|
#reflect(visible) {
|
|
263
522
|
if (this.hasToggleTarget) {
|
|
264
|
-
this.toggleTarget
|
|
265
|
-
this.#
|
|
523
|
+
const toggle = this.toggleTarget;
|
|
524
|
+
this.#pressed.write(toggle, visible ? "true" : "false");
|
|
525
|
+
this.#labels.reflect(toggle, visible);
|
|
266
526
|
}
|
|
267
527
|
this.element.setAttribute("data-state", visible ? "visible" : "hidden");
|
|
268
528
|
}
|
|
529
|
+
get #safeAutoHide() {
|
|
530
|
+
return this.#numbers.read(
|
|
531
|
+
this,
|
|
532
|
+
"autoHide",
|
|
533
|
+
this.autoHideValue,
|
|
534
|
+
_PasswordRevealController.values.autoHide.default,
|
|
535
|
+
_PasswordRevealController.valueConstraints.autoHide
|
|
536
|
+
);
|
|
537
|
+
}
|
|
269
538
|
};
|
|
270
539
|
|
|
271
540
|
export { PasswordRevealController };
|
|
272
|
-
//# sourceMappingURL=password_reveal_controller.js.map
|
|
273
541
|
//# sourceMappingURL=password_reveal_controller.js.map
|