stimeo-ui 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +136 -0
- data/README.md +27 -11
- data/dist/cable/index.js +384 -449
- data/dist/controllers/accordion_controller.js +174 -55
- data/dist/controllers/alert_dialog_controller.js +903 -235
- data/dist/controllers/announcer_controller.js +238 -205
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +414 -145
- data/dist/controllers/avatar_controller.js +185 -107
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +282 -58
- data/dist/controllers/calendar_controller.js +228 -260
- data/dist/controllers/carousel_controller.js +536 -436
- data/dist/controllers/character_counter_controller.js +493 -207
- data/dist/controllers/checkbox_controller.js +38 -74
- data/dist/controllers/clipboard_controller.js +261 -149
- data/dist/controllers/collapsible_controller.js +304 -120
- data/dist/controllers/color_picker_controller.js +178 -146
- data/dist/controllers/combobox_controller.js +522 -156
- data/dist/controllers/command_palette_controller.js +1156 -395
- data/dist/controllers/conditional_fields_controller.js +300 -171
- data/dist/controllers/confirm_controller.js +980 -262
- data/dist/controllers/context_menu_controller.js +346 -153
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +281 -158
- data/dist/controllers/currency_input_controller.js +575 -225
- data/dist/controllers/data_grid_controller.js +248 -214
- data/dist/controllers/date_range_picker_controller.js +338 -235
- data/dist/controllers/dialog_controller.js +899 -237
- data/dist/controllers/direct_upload_controller.js +97 -167
- data/dist/controllers/dirty_form_controller.js +72 -69
- data/dist/controllers/dismissible_controller.js +196 -24
- data/dist/controllers/drawer_controller.js +974 -355
- data/dist/controllers/dropdown_controller.js +339 -96
- data/dist/controllers/editable_controller.js +315 -88
- data/dist/controllers/empty_state_controller.js +79 -58
- data/dist/controllers/file_dropzone_controller.js +322 -211
- data/dist/controllers/filter_controller.js +77 -42
- data/dist/controllers/flash_controller.js +238 -347
- data/dist/controllers/focus_controller.js +679 -240
- data/dist/controllers/form_field_controller.js +156 -132
- data/dist/controllers/form_validation_controller.js +31 -97
- data/dist/controllers/frame_loading_controller.js +275 -217
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +327 -130
- data/dist/controllers/idle_controller.js +271 -101
- data/dist/controllers/input_mask_controller.js +483 -97
- data/dist/controllers/intersection_controller.js +150 -106
- data/dist/controllers/lazy_frame_controller.js +24 -68
- data/dist/controllers/listbox_controller.js +523 -240
- data/dist/controllers/local_time_controller.js +102 -77
- data/dist/controllers/masonry_controller.js +80 -96
- data/dist/controllers/menu_controller.js +302 -188
- data/dist/controllers/menubar_controller.js +232 -345
- data/dist/controllers/meter_controller.js +417 -87
- data/dist/controllers/multi_select_controller.js +785 -393
- data/dist/controllers/navigation_menu_controller.js +190 -250
- data/dist/controllers/nested_form_controller.js +396 -192
- data/dist/controllers/network_status_controller.js +268 -62
- data/dist/controllers/number_input_controller.js +610 -303
- data/dist/controllers/optimistic_controller.js +45 -87
- data/dist/controllers/otp_controller.js +658 -255
- data/dist/controllers/overflow_indicator_controller.js +122 -113
- data/dist/controllers/overflow_menu_controller.js +470 -295
- data/dist/controllers/pagination_controller.js +332 -123
- data/dist/controllers/password_reveal_controller.js +392 -121
- data/dist/controllers/password_strength_controller.js +111 -158
- data/dist/controllers/persist_controller.js +165 -111
- data/dist/controllers/pointer_drag_controller.js +434 -188
- data/dist/controllers/popover_controller.js +269 -113
- data/dist/controllers/portal_controller.js +215 -76
- data/dist/controllers/preview_guard_controller.js +243 -116
- data/dist/controllers/progress_controller.js +391 -78
- data/dist/controllers/radio_group_controller.js +278 -211
- data/dist/controllers/range_slider_controller.js +220 -113
- data/dist/controllers/rating_controller.js +458 -211
- data/dist/controllers/read_more_controller.js +138 -63
- data/dist/controllers/reading_progress_controller.js +166 -137
- data/dist/controllers/relative_time_controller.js +211 -112
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
- data/dist/controllers/resizable_controller.js +299 -150
- data/dist/controllers/roving_controller.js +49 -88
- data/dist/controllers/scroll_area_controller.js +490 -246
- data/dist/controllers/scroll_restore_controller.js +14 -55
- data/dist/controllers/scroll_visibility_controller.js +432 -149
- data/dist/controllers/scrollspy_controller.js +193 -248
- data/dist/controllers/separator_controller.js +285 -142
- data/dist/controllers/sidebar_controller.js +1069 -404
- data/dist/controllers/skeleton_controller.js +259 -101
- data/dist/controllers/slider_controller.js +187 -86
- data/dist/controllers/smart_sticky_header_controller.js +173 -66
- data/dist/controllers/sortable_controller.js +26 -125
- data/dist/controllers/spinner_controller.js +288 -173
- data/dist/controllers/step_indicator_controller.js +105 -46
- data/dist/controllers/stepper_controller.js +120 -73
- data/dist/controllers/stick_to_bottom_controller.js +275 -140
- data/dist/controllers/sticky_observer_controller.js +220 -49
- data/dist/controllers/submit_once_controller.js +376 -227
- data/dist/controllers/switch_controller.js +201 -84
- data/dist/controllers/tabs_controller.js +41 -30
- data/dist/controllers/tags_input_controller.js +293 -173
- data/dist/controllers/textarea_autosize_controller.js +108 -74
- data/dist/controllers/theme_controller.js +140 -116
- data/dist/controllers/time_picker_controller.js +331 -127
- data/dist/controllers/toast_controller.js +244 -324
- data/dist/controllers/toggle_group_controller.js +252 -191
- data/dist/controllers/toolbar_controller.js +219 -120
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +283 -369
- data/dist/index.js +8573 -10410
- data/dist/positioning/index.js +154 -47
- data/lib/generators/stimeo/install/install_generator.rb +7 -2
- data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,73 +1,229 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function setAttributeIfChanged(element, name, value, beforeWrite) {
|
|
4
|
+
if (element.getAttribute(name) === value) return false;
|
|
5
|
+
if (value === null) element.removeAttribute(name);
|
|
6
|
+
else element.setAttribute(name, value);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
function sharedRegistry(key, create) {
|
|
11
|
+
const symbol = Symbol.for(key);
|
|
12
|
+
const scope = globalThis;
|
|
13
|
+
const existing = scope[symbol];
|
|
14
|
+
if (existing !== void 0) return existing;
|
|
15
|
+
const registry2 = create();
|
|
16
|
+
Object.defineProperty(globalThis, symbol, { value: registry2 });
|
|
17
|
+
return registry2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var registry = sharedRegistry(
|
|
21
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
22
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
23
|
+
);
|
|
24
|
+
function isAttributeValue(value) {
|
|
25
|
+
return value === null || typeof value === "string";
|
|
26
|
+
}
|
|
27
|
+
function readLeaseRecord(element, record) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
30
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
31
|
+
const [original, written, ...rest] = parsed;
|
|
32
|
+
const linked = rest[0] !== false;
|
|
33
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
34
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
35
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
36
|
+
} catch {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function attributeStack(element, attribute) {
|
|
41
|
+
const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
42
|
+
registry.stacks.set(element, byAttribute);
|
|
43
|
+
const known = byAttribute.get(attribute);
|
|
44
|
+
if (known) return known;
|
|
45
|
+
const found = [];
|
|
46
|
+
for (const record of registry.names.get(attribute) ?? []) {
|
|
47
|
+
const read = readLeaseRecord(element, record);
|
|
48
|
+
if (read) found.push({ ...read, record });
|
|
49
|
+
}
|
|
50
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
51
|
+
const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
52
|
+
lease: null,
|
|
53
|
+
record,
|
|
54
|
+
written,
|
|
55
|
+
original,
|
|
56
|
+
linked: linked && index > 0
|
|
57
|
+
}));
|
|
58
|
+
byAttribute.set(attribute, stack);
|
|
59
|
+
return stack;
|
|
60
|
+
}
|
|
61
|
+
function findHold(stack, lease, record) {
|
|
62
|
+
return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
|
|
63
|
+
}
|
|
64
|
+
var AttributeLease = class {
|
|
65
|
+
#attribute;
|
|
66
|
+
#record;
|
|
67
|
+
#base;
|
|
68
|
+
#preserveAuthor;
|
|
69
|
+
#held = /* @__PURE__ */ new Set();
|
|
70
|
+
constructor(attribute, owner, options = {}) {
|
|
71
|
+
this.#attribute = attribute;
|
|
72
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
73
|
+
this.#base = options.base === true;
|
|
74
|
+
this.#preserveAuthor = options.preserveAuthor === true;
|
|
75
|
+
const names = registry.names;
|
|
76
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
77
|
+
}
|
|
78
|
+
write(element, value) {
|
|
79
|
+
const stack = attributeStack(element, this.#attribute);
|
|
80
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
81
|
+
this.#held.add(element);
|
|
82
|
+
const shows = !this.#covered(element, stack, hold);
|
|
83
|
+
hold.written = value;
|
|
84
|
+
this.#mark(element, stack);
|
|
85
|
+
if (shows) this.#reflect(element, value);
|
|
86
|
+
}
|
|
87
|
+
return(element) {
|
|
88
|
+
const held = this.#held.delete(element);
|
|
89
|
+
const known = registry.stacks.get(element)?.has(this.#attribute);
|
|
90
|
+
const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
|
|
91
|
+
const hold = this.#take(stack);
|
|
92
|
+
if (!hold) return false;
|
|
93
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
94
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
95
|
+
const index = stack.indexOf(hold);
|
|
96
|
+
stack.splice(index, 1);
|
|
97
|
+
const above = stack[index];
|
|
98
|
+
if (above) above.linked &&= hold.linked;
|
|
99
|
+
this.#mark(element, stack);
|
|
100
|
+
if (!owns) return true;
|
|
101
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
102
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
returnAll() {
|
|
106
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
107
|
+
}
|
|
108
|
+
#take(stack) {
|
|
109
|
+
const hold = findHold(stack, this, this.#record);
|
|
110
|
+
if (hold) hold.lease = this;
|
|
111
|
+
return hold;
|
|
112
|
+
}
|
|
113
|
+
#acquire(element, stack) {
|
|
114
|
+
const current = element.getAttribute(this.#attribute);
|
|
115
|
+
const top = stack.at(-1);
|
|
116
|
+
const bottom = stack[0];
|
|
117
|
+
const over = top?.written === current ? top : void 0;
|
|
118
|
+
const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
|
|
119
|
+
const hold = {
|
|
120
|
+
lease: this,
|
|
121
|
+
record: this.#record,
|
|
122
|
+
written: current,
|
|
123
|
+
original: from ? from.original : current,
|
|
124
|
+
linked: !this.#base && over !== void 0
|
|
125
|
+
};
|
|
126
|
+
if (this.#base && bottom) {
|
|
127
|
+
bottom.linked = true;
|
|
128
|
+
stack.unshift(hold);
|
|
129
|
+
} else stack.push(hold);
|
|
130
|
+
return hold;
|
|
131
|
+
}
|
|
132
|
+
#covered(element, stack, hold) {
|
|
133
|
+
const current = element.getAttribute(this.#attribute);
|
|
134
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
135
|
+
}
|
|
136
|
+
#mark(element, stack) {
|
|
137
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
138
|
+
stack.forEach((hold, index) => {
|
|
139
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
140
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
141
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
142
|
+
records.set(
|
|
143
|
+
hold.record,
|
|
144
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
for (const [name, recorded] of records) {
|
|
148
|
+
setAttributeIfChanged(element, name, recorded);
|
|
24
149
|
}
|
|
25
150
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
151
|
+
#reflect(element, value) {
|
|
152
|
+
setAttributeIfChanged(element, this.#attribute, value);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
var ListenerSet = class {
|
|
157
|
+
#abort = new AbortController();
|
|
158
|
+
add(target, type, handler, options) {
|
|
159
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
160
|
+
}
|
|
161
|
+
dispose() {
|
|
162
|
+
this.#abort.abort();
|
|
163
|
+
this.#abort = new AbortController();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
function matchesNumberBounds(value, bounds) {
|
|
168
|
+
if (!Number.isFinite(value)) {
|
|
169
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
170
|
+
if (direction === null) return false;
|
|
171
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
172
|
+
}
|
|
173
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
174
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
175
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
176
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
177
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readNumber(raw, fallback, bounds) {
|
|
182
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
var NumberValueReader = class {
|
|
186
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
187
|
+
read(owner, name, raw, fallback, bounds) {
|
|
188
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
189
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
190
|
+
this.#lastRejected.delete(name);
|
|
191
|
+
return resolved;
|
|
192
|
+
}
|
|
193
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
194
|
+
const literal = owner.element.getAttribute(attribute);
|
|
195
|
+
if (literal === null) {
|
|
196
|
+
this.#lastRejected.delete(name);
|
|
197
|
+
return resolved;
|
|
198
|
+
}
|
|
199
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
200
|
+
this.#lastRejected.set(name, literal);
|
|
201
|
+
console.warn(
|
|
202
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return resolved;
|
|
31
206
|
}
|
|
32
207
|
};
|
|
33
208
|
|
|
34
|
-
// src/utils/safe_timeout.ts
|
|
35
209
|
var TimerRegistry = class {
|
|
36
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
37
210
|
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
211
|
clear(id) {
|
|
45
212
|
if (this.ids.delete(id)) {
|
|
46
213
|
this.cancel(id);
|
|
47
214
|
}
|
|
48
215
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
51
|
-
* to guarantee no timer outlives the element.
|
|
52
|
-
*/
|
|
53
216
|
clearAll() {
|
|
54
217
|
for (const id of this.ids) {
|
|
55
218
|
this.cancel(id);
|
|
56
219
|
}
|
|
57
220
|
this.ids.clear();
|
|
58
221
|
}
|
|
59
|
-
/** Number of timers currently tracked (pending). */
|
|
60
222
|
get size() {
|
|
61
223
|
return this.ids.size;
|
|
62
224
|
}
|
|
63
225
|
};
|
|
64
226
|
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
227
|
set(callback, delay) {
|
|
72
228
|
const id = this.schedule(() => {
|
|
73
229
|
this.ids.delete(id);
|
|
@@ -84,16 +240,20 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
84
240
|
}
|
|
85
241
|
};
|
|
86
242
|
|
|
87
|
-
|
|
88
|
-
|
|
243
|
+
var StateRegions = class _StateRegions {
|
|
244
|
+
static #registry = sharedRegistry(
|
|
245
|
+
"stimeo-ui.state-regions.registry.v1",
|
|
246
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
247
|
+
);
|
|
89
248
|
#whenTrue;
|
|
90
249
|
#whenFalse;
|
|
91
|
-
#
|
|
92
|
-
constructor(options) {
|
|
250
|
+
#record;
|
|
251
|
+
constructor(options, owner) {
|
|
93
252
|
this.#whenTrue = options.whenTrue;
|
|
94
253
|
this.#whenFalse = options.whenFalse ?? null;
|
|
254
|
+
this.#record = `data-${owner}-hidden-region`;
|
|
255
|
+
_StateRegions.#registry.names.add(this.#record);
|
|
95
256
|
}
|
|
96
|
-
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
97
257
|
reflect(host, isTrue) {
|
|
98
258
|
const shown = this.#inside(host, this.#whenTrue());
|
|
99
259
|
if (!this.#whenFalse) {
|
|
@@ -109,111 +269,229 @@ var StateRegions = class {
|
|
|
109
269
|
for (const region of shown) this.#write(region, !isTrue);
|
|
110
270
|
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
111
271
|
}
|
|
112
|
-
|
|
272
|
+
release(host) {
|
|
273
|
+
for (const region of [host, ...host.querySelectorAll("*")]) {
|
|
274
|
+
if (!(region instanceof HTMLElement)) continue;
|
|
275
|
+
const stack = this.#held(region);
|
|
276
|
+
const hold = stack && this.#take(stack);
|
|
277
|
+
if (!stack || !hold) continue;
|
|
278
|
+
const owns = region.hidden === hold.written && !this.#covered(region, stack, hold);
|
|
279
|
+
const index = stack.indexOf(hold);
|
|
280
|
+
stack.splice(index, 1);
|
|
281
|
+
const above = stack[index];
|
|
282
|
+
if (above) above.linked &&= hold.linked;
|
|
283
|
+
if (owns) {
|
|
284
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
285
|
+
if (below) {
|
|
286
|
+
if (region.hidden !== below.written) region.hidden = below.written;
|
|
287
|
+
} else {
|
|
288
|
+
setAttributeIfChanged(region, "hidden", hold.authored);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
this.#mark(region, stack);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
113
294
|
#write(region, hidden) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
295
|
+
const stack = this.#stack(region);
|
|
296
|
+
const hold = this.#take(stack) ?? this.#acquire(region, stack);
|
|
297
|
+
const shows = !this.#covered(region, stack, hold);
|
|
298
|
+
hold.written = hidden;
|
|
299
|
+
if (shows && region.hidden !== hidden) region.hidden = hidden;
|
|
300
|
+
this.#mark(region, stack);
|
|
117
301
|
}
|
|
118
|
-
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
119
302
|
#give(region) {
|
|
120
|
-
|
|
303
|
+
const stack = this.#held(region);
|
|
304
|
+
if (stack && this.#take(stack)?.written) this.#write(region, false);
|
|
305
|
+
}
|
|
306
|
+
#held(region) {
|
|
307
|
+
return _StateRegions.#registry.stacks.has(region) || region.hasAttribute(this.#record) ? this.#stack(region) : void 0;
|
|
308
|
+
}
|
|
309
|
+
#stack(region) {
|
|
310
|
+
const stacks = _StateRegions.#registry.stacks;
|
|
311
|
+
const stack = stacks.get(region) ?? this.#seed(region);
|
|
312
|
+
stacks.set(region, stack);
|
|
313
|
+
return stack;
|
|
314
|
+
}
|
|
315
|
+
#seed(region) {
|
|
316
|
+
const found = [];
|
|
317
|
+
for (const record of _StateRegions.#registry.names) {
|
|
318
|
+
const read = this.#recorded(region, record);
|
|
319
|
+
if (read) found.push({ ...read, record });
|
|
320
|
+
}
|
|
321
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
322
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, authored, written, linked }, index) => ({
|
|
323
|
+
instance: null,
|
|
324
|
+
record,
|
|
325
|
+
authored,
|
|
326
|
+
written,
|
|
327
|
+
linked: linked && index > 0
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
#take(stack) {
|
|
331
|
+
const hold = stack.find((held) => held.instance === this) ?? stack.find((held) => held.instance === null && held.record === this.#record);
|
|
332
|
+
if (hold) hold.instance = this;
|
|
333
|
+
return hold;
|
|
334
|
+
}
|
|
335
|
+
#covered(region, stack, hold) {
|
|
336
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === region.hidden);
|
|
337
|
+
}
|
|
338
|
+
#acquire(region, stack) {
|
|
339
|
+
const top = stack.at(-1);
|
|
340
|
+
const over = top && region.hidden === top.written ? top : void 0;
|
|
341
|
+
const hold = {
|
|
342
|
+
instance: this,
|
|
343
|
+
record: this.#record,
|
|
344
|
+
authored: over ? over.authored : region.getAttribute("hidden"),
|
|
345
|
+
written: false,
|
|
346
|
+
linked: over !== void 0
|
|
347
|
+
};
|
|
348
|
+
stack.push(hold);
|
|
349
|
+
return hold;
|
|
350
|
+
}
|
|
351
|
+
#mark(region, stack) {
|
|
352
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
353
|
+
stack.forEach((hold, index) => {
|
|
354
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
355
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
356
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
357
|
+
records.set(
|
|
358
|
+
hold.record,
|
|
359
|
+
region.getAttribute("hidden") === hold.authored && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.authored, hold.written, ...marker, ...beneath])
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
for (const [name, recorded] of records) {
|
|
363
|
+
setAttributeIfChanged(region, name, recorded);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
#recorded(region, name) {
|
|
367
|
+
try {
|
|
368
|
+
const parsed = JSON.parse(region.getAttribute(name) ?? "null");
|
|
369
|
+
if (!Array.isArray(parsed)) return null;
|
|
370
|
+
const [authored, written, ...rest] = parsed;
|
|
371
|
+
if (authored !== null && typeof authored !== "string") return null;
|
|
372
|
+
const linked = rest[0] !== false;
|
|
373
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
374
|
+
if (!beneath.every((under) => typeof under === "string")) return null;
|
|
375
|
+
return typeof written === "boolean" ? { authored, written, linked, beneath } : null;
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
121
379
|
}
|
|
122
|
-
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
123
380
|
#inside(host, regions) {
|
|
124
381
|
return regions.filter((region) => host.contains(region));
|
|
125
382
|
}
|
|
126
383
|
};
|
|
127
384
|
|
|
128
|
-
// src/controllers/password_reveal_controller.ts
|
|
129
385
|
var MAX_DELAY = 2 ** 31 - 1;
|
|
130
|
-
|
|
386
|
+
function maskCopyOf(type, input) {
|
|
387
|
+
const revealed = input.type === "text";
|
|
388
|
+
type.return(input);
|
|
389
|
+
if (revealed && input.type === "password") input.value = "";
|
|
390
|
+
}
|
|
391
|
+
var PasswordRevealController = class _PasswordRevealController extends Controller {
|
|
392
|
+
#numbers = new NumberValueReader();
|
|
131
393
|
static targets = ["input", "toggle", "onLabel", "offLabel"];
|
|
132
394
|
static values = {
|
|
133
395
|
autoHide: { type: Number, default: 0 }
|
|
134
396
|
};
|
|
397
|
+
static valueConstraints = {
|
|
398
|
+
autoHide: { finite: true, allowInfinity: "positive" }
|
|
399
|
+
};
|
|
135
400
|
static actions = ["toggle"];
|
|
136
401
|
static events = ["toggle"];
|
|
137
|
-
|
|
402
|
+
static #maskedRenders = sharedRegistry(
|
|
403
|
+
"stimeo-ui.password-reveal-render-mask.registry.v1",
|
|
404
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
405
|
+
);
|
|
138
406
|
#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
407
|
#connected = false;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
408
|
+
#labels = new StateRegions(
|
|
409
|
+
{
|
|
410
|
+
whenTrue: () => this.onLabelTargets,
|
|
411
|
+
whenFalse: () => this.offLabelTargets
|
|
412
|
+
},
|
|
413
|
+
this.identifier
|
|
414
|
+
);
|
|
415
|
+
#pressed = new AttributeLease("aria-pressed", this.identifier);
|
|
416
|
+
#type = new AttributeLease("type", this.identifier);
|
|
417
|
+
#listeners = new ListenerSet();
|
|
418
|
+
#lived = false;
|
|
156
419
|
connect() {
|
|
157
420
|
this.#connected = true;
|
|
421
|
+
if (!this.#lived) this.#maskCopy();
|
|
422
|
+
this.#lived = true;
|
|
158
423
|
const visible = this.#isVisible;
|
|
159
424
|
this.#reflect(visible);
|
|
160
|
-
this.#
|
|
425
|
+
this.#listeners.add(document, "turbo:before-cache", this.#rewindToMasked);
|
|
426
|
+
_PasswordRevealController.#maskRendersInto(this.element.ownerDocument, this.identifier);
|
|
161
427
|
this.#arm(visible);
|
|
162
428
|
}
|
|
163
429
|
disconnect() {
|
|
164
430
|
this.#connected = false;
|
|
165
431
|
this.#timers.clearAll();
|
|
166
|
-
this.#
|
|
432
|
+
this.#listeners.dispose();
|
|
167
433
|
}
|
|
168
|
-
/** Re-derives the hooks and the re-mask for a field swapped in after connect. */
|
|
169
434
|
inputTargetConnected() {
|
|
435
|
+
for (const input of this.inputTargets.slice(1)) this.#type.return(input);
|
|
170
436
|
const visible = this.#connected && this.#isVisible;
|
|
171
437
|
this.#reflect(visible);
|
|
172
438
|
this.#arm(visible);
|
|
173
439
|
}
|
|
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;
|
|
440
|
+
inputTargetDisconnected(input) {
|
|
441
|
+
if (!this.inputTargets.includes(input)) this.#type.return(input);
|
|
442
|
+
if (!this.#connected) return;
|
|
443
|
+
const visible = this.#isVisible;
|
|
186
444
|
this.#reflect(visible);
|
|
187
445
|
this.#arm(visible);
|
|
188
446
|
}
|
|
189
|
-
/** Re-derives the pressed state for a button swapped in after connect. */
|
|
190
447
|
toggleTargetConnected() {
|
|
191
448
|
this.#reflect(this.#isVisible);
|
|
192
449
|
}
|
|
193
|
-
|
|
450
|
+
toggleTargetDisconnected(toggle) {
|
|
451
|
+
if (!this.toggleTargets.includes(toggle)) {
|
|
452
|
+
this.#pressed.return(toggle);
|
|
453
|
+
this.#labels.release(toggle);
|
|
454
|
+
}
|
|
455
|
+
if (this.#connected) this.#reflect(this.#isVisible);
|
|
456
|
+
}
|
|
194
457
|
onLabelTargetConnected() {
|
|
195
458
|
this.#reflect(this.#isVisible);
|
|
196
459
|
}
|
|
197
|
-
/** Describes the state to a masked-side label that arrives after connect. */
|
|
198
460
|
offLabelTargetConnected() {
|
|
199
461
|
this.#reflect(this.#isVisible);
|
|
200
462
|
}
|
|
201
|
-
/** Toggles the input between masked and revealed. Bound via `data-action`. */
|
|
202
463
|
toggle() {
|
|
203
464
|
this.#setVisible(!this.#isVisible);
|
|
204
465
|
}
|
|
205
|
-
/** Whether the input is currently revealed (`type="text"`). */
|
|
206
466
|
get #isVisible() {
|
|
207
467
|
return this.hasInputTarget && this.inputTarget.type === "text";
|
|
208
468
|
}
|
|
209
|
-
|
|
469
|
+
#maskCopy() {
|
|
470
|
+
if (this.hasInputTarget) maskCopyOf(this.#type, this.inputTarget);
|
|
471
|
+
}
|
|
472
|
+
static #maskRendersInto(document2, identifier) {
|
|
473
|
+
const masked = _PasswordRevealController.#maskedRenders.get(document2) ?? /* @__PURE__ */ new Set();
|
|
474
|
+
if (masked.has(identifier)) return;
|
|
475
|
+
masked.add(identifier);
|
|
476
|
+
_PasswordRevealController.#maskedRenders.set(document2, masked);
|
|
477
|
+
const type = new AttributeLease("type", identifier);
|
|
478
|
+
document2.addEventListener("turbo:before-render", (event) => {
|
|
479
|
+
const body = event.detail?.newBody;
|
|
480
|
+
if (!(body instanceof Element)) return;
|
|
481
|
+
for (const input of body.querySelectorAll(
|
|
482
|
+
`input[data-${identifier}-type-lease]`
|
|
483
|
+
)) {
|
|
484
|
+
if (input.type === "text") maskCopyOf(type, input);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
210
488
|
#setVisible(visible) {
|
|
211
489
|
if (!this.hasInputTarget) return;
|
|
212
490
|
const input = this.inputTarget;
|
|
213
491
|
const restoreInputFocus = document.activeElement === input;
|
|
214
492
|
const selectionStart = input.selectionStart;
|
|
215
493
|
const selectionEnd = input.selectionEnd;
|
|
216
|
-
input
|
|
494
|
+
this.#type.write(input, visible ? "text" : "password");
|
|
217
495
|
if (restoreInputFocus) {
|
|
218
496
|
input.focus();
|
|
219
497
|
if (selectionStart !== null && selectionEnd !== null) {
|
|
@@ -227,7 +505,6 @@ var PasswordRevealController = class extends Controller {
|
|
|
227
505
|
this.dispatch("toggle", { detail: { visible } });
|
|
228
506
|
this.#arm(visible);
|
|
229
507
|
}
|
|
230
|
-
/** Schedules the auto re-mask for a revealed field, replacing any pending one. */
|
|
231
508
|
#arm(visible) {
|
|
232
509
|
this.#timers.clearAll();
|
|
233
510
|
const delay = this.#autoHideDelay;
|
|
@@ -235,39 +512,33 @@ var PasswordRevealController = class extends Controller {
|
|
|
235
512
|
this.#timers.set(() => this.#setVisible(false), delay);
|
|
236
513
|
}
|
|
237
514
|
}
|
|
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
515
|
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() {
|
|
516
|
+
return Math.min(this.#safeAutoHide, MAX_DELAY);
|
|
517
|
+
}
|
|
518
|
+
#rewindToMasked = () => {
|
|
256
519
|
this.#timers.clearAll();
|
|
257
520
|
if (!this.hasInputTarget) return;
|
|
258
|
-
this.inputTarget
|
|
521
|
+
this.#type.write(this.inputTarget, "password");
|
|
259
522
|
this.#reflect(false);
|
|
260
|
-
}
|
|
261
|
-
/** Reflects the visible state onto `aria-pressed`, `data-state` and the labels. */
|
|
523
|
+
};
|
|
262
524
|
#reflect(visible) {
|
|
263
525
|
if (this.hasToggleTarget) {
|
|
264
|
-
this.toggleTarget
|
|
265
|
-
this.#
|
|
526
|
+
const toggle = this.toggleTarget;
|
|
527
|
+
this.#pressed.write(toggle, visible ? "true" : "false");
|
|
528
|
+
this.#labels.reflect(toggle, visible);
|
|
266
529
|
}
|
|
267
530
|
this.element.setAttribute("data-state", visible ? "visible" : "hidden");
|
|
268
531
|
}
|
|
532
|
+
get #safeAutoHide() {
|
|
533
|
+
return this.#numbers.read(
|
|
534
|
+
this,
|
|
535
|
+
"autoHide",
|
|
536
|
+
this.autoHideValue,
|
|
537
|
+
_PasswordRevealController.values.autoHide.default,
|
|
538
|
+
_PasswordRevealController.valueConstraints.autoHide
|
|
539
|
+
);
|
|
540
|
+
}
|
|
269
541
|
};
|
|
270
542
|
|
|
271
543
|
export { PasswordRevealController };
|
|
272
|
-
//# sourceMappingURL=password_reveal_controller.js.map
|
|
273
544
|
//# sourceMappingURL=password_reveal_controller.js.map
|