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,19 +1,14 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/stick_to_bottom_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/layout_observer.ts
|
|
6
3
|
var LayoutObserver = class {
|
|
7
4
|
#callback;
|
|
8
5
|
#resizeObserverFactory;
|
|
9
6
|
#resizeObserver = null;
|
|
10
7
|
#observingViewport = false;
|
|
11
8
|
#loadContainer = null;
|
|
12
|
-
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
13
9
|
#handleViewportResize = () => {
|
|
14
10
|
this.#callback();
|
|
15
11
|
};
|
|
16
|
-
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
17
12
|
#handleDescendantLoad = () => {
|
|
18
13
|
this.#callback();
|
|
19
14
|
};
|
|
@@ -21,11 +16,6 @@ var LayoutObserver = class {
|
|
|
21
16
|
this.#callback = callback;
|
|
22
17
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
23
18
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Starts observing an element's size. Repeated calls observe additional
|
|
26
|
-
* elements through the same shared observer. No-ops when no
|
|
27
|
-
* `ResizeObserver` implementation is available.
|
|
28
|
-
*/
|
|
29
19
|
observe(element) {
|
|
30
20
|
if (!this.#resizeObserverFactory) return;
|
|
31
21
|
if (!this.#resizeObserver) {
|
|
@@ -35,47 +25,28 @@ var LayoutObserver = class {
|
|
|
35
25
|
}
|
|
36
26
|
this.#resizeObserver.observe(element);
|
|
37
27
|
}
|
|
38
|
-
/** Stops observing a single element while leaving any others in place. */
|
|
39
28
|
unobserve(element) {
|
|
40
29
|
this.#resizeObserver?.unobserve(element);
|
|
41
30
|
}
|
|
42
|
-
/** Starts observing viewport resizes. Idempotent: the listener is added once. */
|
|
43
31
|
observeViewport() {
|
|
44
32
|
if (this.#observingViewport) return;
|
|
45
33
|
this.#observingViewport = true;
|
|
46
34
|
window.addEventListener("resize", this.#handleViewportResize);
|
|
47
35
|
}
|
|
48
|
-
/** Stops observing viewport resizes without affecting element observation. */
|
|
49
36
|
unobserveViewport() {
|
|
50
37
|
if (!this.#observingViewport) return;
|
|
51
38
|
this.#observingViewport = false;
|
|
52
39
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
53
40
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Starts reporting a `load` from anywhere inside `container` — an image or a
|
|
56
|
-
* frame settling changes the box it sits in, and it measures as zero high until
|
|
57
|
-
* then. `load` does not bubble, so the subscription is a capture-phase listener
|
|
58
|
-
* on the container itself and nothing the caller spells.
|
|
59
|
-
*
|
|
60
|
-
* **One container at a time.** A further call moves the observation, so a widget
|
|
61
|
-
* whose content element is swapped at runtime releases the element it let go by
|
|
62
|
-
* naming the new one — there is no second place for the release to drift from.
|
|
63
|
-
*/
|
|
64
41
|
observeDescendantLoads(container) {
|
|
65
42
|
this.unobserveDescendantLoads();
|
|
66
43
|
this.#loadContainer = container;
|
|
67
44
|
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
68
45
|
}
|
|
69
|
-
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
70
46
|
unobserveDescendantLoads() {
|
|
71
47
|
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
72
48
|
this.#loadContainer = null;
|
|
73
49
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Releases every observation: disconnects the {@link ResizeObserver} and removes
|
|
76
|
-
* the viewport and descendant-load listeners. Safe to call multiple times. Call
|
|
77
|
-
* this from a controller's `disconnect()`.
|
|
78
|
-
*/
|
|
79
50
|
disconnect() {
|
|
80
51
|
this.#resizeObserver?.disconnect();
|
|
81
52
|
this.#resizeObserver = null;
|
|
@@ -84,21 +55,142 @@ var LayoutObserver = class {
|
|
|
84
55
|
}
|
|
85
56
|
};
|
|
86
57
|
|
|
87
|
-
|
|
58
|
+
var ListenerSet = class {
|
|
59
|
+
#abort = new AbortController();
|
|
60
|
+
add(target, type, handler, options) {
|
|
61
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
62
|
+
}
|
|
63
|
+
dispose() {
|
|
64
|
+
this.#abort.abort();
|
|
65
|
+
this.#abort = new AbortController();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
var MicrotaskCoalescer = class {
|
|
70
|
+
#run;
|
|
71
|
+
#queued = false;
|
|
72
|
+
#active = false;
|
|
73
|
+
#generation = 0;
|
|
74
|
+
constructor(run) {
|
|
75
|
+
this.#run = run;
|
|
76
|
+
}
|
|
77
|
+
activate() {
|
|
78
|
+
this.#active = true;
|
|
79
|
+
}
|
|
80
|
+
cancel() {
|
|
81
|
+
this.#active = false;
|
|
82
|
+
this.#queued = false;
|
|
83
|
+
this.#generation += 1;
|
|
84
|
+
}
|
|
85
|
+
schedule() {
|
|
86
|
+
if (!this.#active || this.#queued) return;
|
|
87
|
+
this.#queued = true;
|
|
88
|
+
const generation = this.#generation;
|
|
89
|
+
queueMicrotask(() => {
|
|
90
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
91
|
+
this.#queued = false;
|
|
92
|
+
this.#run();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
var MorphRenderWatcher = class {
|
|
98
|
+
#listeners = new ListenerSet();
|
|
99
|
+
#pass;
|
|
100
|
+
#element = null;
|
|
101
|
+
#onMorph = () => this.#pass.schedule();
|
|
102
|
+
constructor(run) {
|
|
103
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
104
|
+
}
|
|
105
|
+
observe(element) {
|
|
106
|
+
if (this.#element === element) return;
|
|
107
|
+
this.disconnect();
|
|
108
|
+
this.#element = element;
|
|
109
|
+
this.#pass.activate();
|
|
110
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
111
|
+
}
|
|
112
|
+
schedule() {
|
|
113
|
+
this.#pass.schedule();
|
|
114
|
+
}
|
|
115
|
+
disconnect() {
|
|
116
|
+
this.#listeners.dispose();
|
|
117
|
+
this.#pass.cancel();
|
|
118
|
+
this.#element = null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
var NUMBER_BOUNDS = {
|
|
123
|
+
nonNegative: { finite: true, min: 0 }};
|
|
124
|
+
function matchesNumberBounds(value, bounds) {
|
|
125
|
+
if (!Number.isFinite(value)) {
|
|
126
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
127
|
+
if (direction === null) return false;
|
|
128
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
129
|
+
}
|
|
130
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
131
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
132
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
133
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
134
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readNumber(raw, fallback, bounds) {
|
|
139
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
var NumberValueReader = class {
|
|
143
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
144
|
+
read(owner, name, raw, fallback, bounds) {
|
|
145
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
146
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
147
|
+
this.#lastRejected.delete(name);
|
|
148
|
+
return resolved;
|
|
149
|
+
}
|
|
150
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
151
|
+
const literal = owner.element.getAttribute(attribute);
|
|
152
|
+
if (literal === null) {
|
|
153
|
+
this.#lastRejected.delete(name);
|
|
154
|
+
return resolved;
|
|
155
|
+
}
|
|
156
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
157
|
+
this.#lastRejected.set(name, literal);
|
|
158
|
+
console.warn(
|
|
159
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return resolved;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
88
166
|
function prefersReducedMotion() {
|
|
89
167
|
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
90
168
|
}
|
|
91
169
|
|
|
92
|
-
|
|
93
|
-
|
|
170
|
+
function sharedRegistry(key, create) {
|
|
171
|
+
const symbol = Symbol.for(key);
|
|
172
|
+
const scope = globalThis;
|
|
173
|
+
const existing = scope[symbol];
|
|
174
|
+
if (existing !== void 0) return existing;
|
|
175
|
+
const registry = create();
|
|
176
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
177
|
+
return registry;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
var StateRegions = class _StateRegions {
|
|
181
|
+
static #registry = sharedRegistry(
|
|
182
|
+
"stimeo-ui.state-regions.registry.v1",
|
|
183
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
184
|
+
);
|
|
94
185
|
#whenTrue;
|
|
95
186
|
#whenFalse;
|
|
96
|
-
#
|
|
97
|
-
constructor(options) {
|
|
187
|
+
#record;
|
|
188
|
+
constructor(options, owner) {
|
|
98
189
|
this.#whenTrue = options.whenTrue;
|
|
99
190
|
this.#whenFalse = options.whenFalse ?? null;
|
|
191
|
+
this.#record = `data-${owner}-hidden-region`;
|
|
192
|
+
_StateRegions.#registry.names.add(this.#record);
|
|
100
193
|
}
|
|
101
|
-
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
102
194
|
reflect(host, isTrue) {
|
|
103
195
|
const shown = this.#inside(host, this.#whenTrue());
|
|
104
196
|
if (!this.#whenFalse) {
|
|
@@ -114,51 +206,159 @@ var StateRegions = class {
|
|
|
114
206
|
for (const region of shown) this.#write(region, !isTrue);
|
|
115
207
|
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
116
208
|
}
|
|
117
|
-
|
|
209
|
+
release(host) {
|
|
210
|
+
for (const region of [host, ...host.querySelectorAll("*")]) {
|
|
211
|
+
if (!(region instanceof HTMLElement)) continue;
|
|
212
|
+
const stack = this.#held(region);
|
|
213
|
+
const hold = stack && this.#take(stack);
|
|
214
|
+
if (!stack || !hold) continue;
|
|
215
|
+
const owns = region.hidden === hold.written && !this.#covered(region, stack, hold);
|
|
216
|
+
const index = stack.indexOf(hold);
|
|
217
|
+
stack.splice(index, 1);
|
|
218
|
+
const above = stack[index];
|
|
219
|
+
if (above) above.linked &&= hold.linked;
|
|
220
|
+
if (owns) {
|
|
221
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
222
|
+
if (below) {
|
|
223
|
+
if (region.hidden !== below.written) region.hidden = below.written;
|
|
224
|
+
} else if (region.getAttribute("hidden") !== hold.authored) {
|
|
225
|
+
if (hold.authored === null) region.removeAttribute("hidden");
|
|
226
|
+
else region.setAttribute("hidden", hold.authored);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
this.#mark(region, stack);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
118
232
|
#write(region, hidden) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
233
|
+
const stack = this.#stack(region);
|
|
234
|
+
const hold = this.#take(stack) ?? this.#acquire(region, stack);
|
|
235
|
+
const shows = !this.#covered(region, stack, hold);
|
|
236
|
+
hold.written = hidden;
|
|
237
|
+
if (shows && region.hidden !== hidden) region.hidden = hidden;
|
|
238
|
+
this.#mark(region, stack);
|
|
122
239
|
}
|
|
123
|
-
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
124
240
|
#give(region) {
|
|
125
|
-
|
|
241
|
+
const stack = this.#held(region);
|
|
242
|
+
if (stack && this.#take(stack)?.written) this.#write(region, false);
|
|
243
|
+
}
|
|
244
|
+
#held(region) {
|
|
245
|
+
return _StateRegions.#registry.stacks.has(region) || region.hasAttribute(this.#record) ? this.#stack(region) : void 0;
|
|
246
|
+
}
|
|
247
|
+
#stack(region) {
|
|
248
|
+
const stacks = _StateRegions.#registry.stacks;
|
|
249
|
+
const stack = stacks.get(region) ?? this.#seed(region);
|
|
250
|
+
stacks.set(region, stack);
|
|
251
|
+
return stack;
|
|
252
|
+
}
|
|
253
|
+
#seed(region) {
|
|
254
|
+
const found = [];
|
|
255
|
+
for (const record of _StateRegions.#registry.names) {
|
|
256
|
+
const read = this.#recorded(region, record);
|
|
257
|
+
if (read) found.push({ ...read, record });
|
|
258
|
+
}
|
|
259
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
260
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, authored, written, linked }, index) => ({
|
|
261
|
+
instance: null,
|
|
262
|
+
record,
|
|
263
|
+
authored,
|
|
264
|
+
written,
|
|
265
|
+
linked: linked && index > 0
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
#take(stack) {
|
|
269
|
+
const hold = stack.find((held) => held.instance === this) ?? stack.find((held) => held.instance === null && held.record === this.#record);
|
|
270
|
+
if (hold) hold.instance = this;
|
|
271
|
+
return hold;
|
|
272
|
+
}
|
|
273
|
+
#covered(region, stack, hold) {
|
|
274
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === region.hidden);
|
|
275
|
+
}
|
|
276
|
+
#acquire(region, stack) {
|
|
277
|
+
const top = stack.at(-1);
|
|
278
|
+
const over = top && region.hidden === top.written ? top : void 0;
|
|
279
|
+
const hold = {
|
|
280
|
+
instance: this,
|
|
281
|
+
record: this.#record,
|
|
282
|
+
authored: over ? over.authored : region.getAttribute("hidden"),
|
|
283
|
+
written: false,
|
|
284
|
+
linked: over !== void 0
|
|
285
|
+
};
|
|
286
|
+
stack.push(hold);
|
|
287
|
+
return hold;
|
|
288
|
+
}
|
|
289
|
+
#mark(region, stack) {
|
|
290
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
291
|
+
stack.forEach((hold, index) => {
|
|
292
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
293
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
294
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
295
|
+
records.set(
|
|
296
|
+
hold.record,
|
|
297
|
+
region.getAttribute("hidden") === hold.authored && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.authored, hold.written, ...marker, ...beneath])
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
for (const [name, recorded] of records) {
|
|
301
|
+
if (region.getAttribute(name) === recorded) continue;
|
|
302
|
+
if (recorded === null) region.removeAttribute(name);
|
|
303
|
+
else region.setAttribute(name, recorded);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
#recorded(region, name) {
|
|
307
|
+
try {
|
|
308
|
+
const parsed = JSON.parse(region.getAttribute(name) ?? "null");
|
|
309
|
+
if (!Array.isArray(parsed)) return null;
|
|
310
|
+
const [authored, written, ...rest] = parsed;
|
|
311
|
+
if (authored !== null && typeof authored !== "string") return null;
|
|
312
|
+
const linked = rest[0] !== false;
|
|
313
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
314
|
+
if (!beneath.every((under) => typeof under === "string")) return null;
|
|
315
|
+
return typeof written === "boolean" ? { authored, written, linked, beneath } : null;
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
126
319
|
}
|
|
127
|
-
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
128
320
|
#inside(host, regions) {
|
|
129
321
|
return regions.filter((region) => host.contains(region));
|
|
130
322
|
}
|
|
131
323
|
};
|
|
132
324
|
|
|
133
|
-
// src/controllers/stick_to_bottom_controller.ts
|
|
134
325
|
var DEFAULT_THRESHOLD = 80;
|
|
135
326
|
var countElements = (nodes) => {
|
|
136
327
|
let n = 0;
|
|
137
328
|
for (const node of nodes) if (node.nodeType === Node.ELEMENT_NODE) n += 1;
|
|
138
329
|
return n;
|
|
139
330
|
};
|
|
140
|
-
var StickToBottomController = class extends Controller {
|
|
331
|
+
var StickToBottomController = class _StickToBottomController extends Controller {
|
|
332
|
+
#numbers = new NumberValueReader();
|
|
141
333
|
static targets = ["content", "hasNew"];
|
|
142
334
|
static values = {
|
|
143
335
|
threshold: { type: Number, default: DEFAULT_THRESHOLD },
|
|
144
336
|
behavior: { type: String, default: "auto" },
|
|
145
337
|
pinOnConnect: { type: Boolean, default: false }
|
|
146
338
|
};
|
|
339
|
+
static valueConstraints = {
|
|
340
|
+
threshold: NUMBER_BOUNDS.nonNegative
|
|
341
|
+
};
|
|
147
342
|
static actions = ["scrollToBottom"];
|
|
148
343
|
static events = ["pin", "new"];
|
|
149
344
|
#observer = null;
|
|
150
|
-
/** The element the append observer currently holds, so a swap can be detected. */
|
|
151
345
|
#watched = null;
|
|
152
|
-
/** Watches for the box a container connected without one is still waiting on. */
|
|
153
346
|
#layout = new LayoutObserver(() => this.#onLaidOut());
|
|
154
347
|
#awaitingLayout = false;
|
|
155
348
|
#connected = false;
|
|
156
349
|
#pinned = false;
|
|
157
350
|
#hasNew = false;
|
|
158
|
-
|
|
159
|
-
|
|
351
|
+
#hasNewRegion = new StateRegions(
|
|
352
|
+
{ whenTrue: () => this.hasNewTargets },
|
|
353
|
+
this.identifier
|
|
354
|
+
);
|
|
160
355
|
#onScroll = () => this.#updatePinned();
|
|
356
|
+
#morphRender = new MorphRenderWatcher(() => {
|
|
357
|
+
this.#updatePinned();
|
|
358
|
+
this.#setHasNew(this.#hasNew);
|
|
359
|
+
});
|
|
161
360
|
connect() {
|
|
361
|
+
this.#morphRender.observe(this.element);
|
|
162
362
|
this.#connected = true;
|
|
163
363
|
if (this.pinOnConnectValue && this.#measurable()) this.#scrollToBottom("instant");
|
|
164
364
|
this.#setHasNew(false);
|
|
@@ -169,57 +369,31 @@ var StickToBottomController = class extends Controller {
|
|
|
169
369
|
if (!this.#measurable()) this.#waitForLayout();
|
|
170
370
|
}
|
|
171
371
|
disconnect() {
|
|
372
|
+
this.#morphRender.disconnect();
|
|
172
373
|
this.#connected = false;
|
|
173
374
|
this.element.removeEventListener("scroll", this.#onScroll);
|
|
174
375
|
this.#stopWatching();
|
|
175
376
|
this.#stopWaitingForLayout();
|
|
176
377
|
}
|
|
177
|
-
/** Moves the append watch onto a `content` target that arrived at runtime. */
|
|
178
378
|
contentTargetConnected() {
|
|
179
379
|
this.#syncWatched();
|
|
180
380
|
}
|
|
181
|
-
/** Moves the append watch off a `content` target that left, back onto the container. */
|
|
182
381
|
contentTargetDisconnected() {
|
|
183
382
|
this.#syncWatched();
|
|
184
383
|
}
|
|
185
|
-
/**
|
|
186
|
-
* Settles a has-new region that arrived at runtime on the side the flag is currently
|
|
187
|
-
* on, the way a region present at connect is settled by the connect-time normalization.
|
|
188
|
-
*
|
|
189
|
-
* Stimulus attaches targets before `connect()`, and a container that leaves the document
|
|
190
|
-
* and comes back keeps its controller, so in that window the flag still reads the
|
|
191
|
-
* arrival of the connection that ended. A region attached there is left to the
|
|
192
|
-
* connect-time normalization, which drops the flag and settles the region in one write.
|
|
193
|
-
*/
|
|
194
384
|
hasNewTargetConnected() {
|
|
195
385
|
if (!this.#connected) return;
|
|
196
386
|
this.#hasNewRegion.reflect(this.element, this.#hasNew);
|
|
197
387
|
}
|
|
198
|
-
/**
|
|
199
|
-
* Re-derives pinned when the distance that counts as the bottom is changed at runtime
|
|
200
|
-
* (a morph that swaps the attribute on a retained element).
|
|
201
|
-
*/
|
|
202
388
|
thresholdValueChanged() {
|
|
203
389
|
if (!this.#connected) return;
|
|
204
390
|
this.#updatePinned();
|
|
205
391
|
}
|
|
206
|
-
/**
|
|
207
|
-
* Jumps to the bottom and re-pins (wired to a "new messages" button).
|
|
208
|
-
*
|
|
209
|
-
* The has-new flag clears on request — the user has acknowledged the arrival — while
|
|
210
|
-
* pinned is read back from where the scroll landed: a jump that arrives by the time
|
|
211
|
-
* this returns pins immediately, an animated one settles from its own scroll events,
|
|
212
|
-
* and a jump the engine cannot honor leaves the container unpinned, so the next append
|
|
213
|
-
* flags it again instead of being swallowed by a pinned state that does not hold.
|
|
214
|
-
*
|
|
215
|
-
* Which of those happens is not this method's to decide — see {@link behaviorValue}.
|
|
216
|
-
*/
|
|
217
392
|
scrollToBottom() {
|
|
218
393
|
this.#scrollToBottom();
|
|
219
394
|
this.#setHasNew(false);
|
|
220
395
|
this.#updatePinned();
|
|
221
396
|
}
|
|
222
|
-
/** Follows appended children while pinned; otherwise flags new content. */
|
|
223
397
|
#onMutations(mutations) {
|
|
224
398
|
let added = 0;
|
|
225
399
|
for (const mutation of mutations) added += countElements(mutation.addedNodes);
|
|
@@ -231,19 +405,13 @@ var StickToBottomController = class extends Controller {
|
|
|
231
405
|
this.dispatch("new", { detail: { count: added } });
|
|
232
406
|
}
|
|
233
407
|
}
|
|
234
|
-
/**
|
|
235
|
-
* Recomputes pinned from the scroll position and reflects it on a transition.
|
|
236
|
-
*
|
|
237
|
-
* @stimeoRenderRoot
|
|
238
|
-
*/
|
|
239
408
|
#updatePinned() {
|
|
240
409
|
const pinned = this.#isPinned();
|
|
241
|
-
|
|
410
|
+
const changed = pinned !== this.#pinned;
|
|
242
411
|
this.#pinned = pinned;
|
|
243
412
|
this.#reflectPinned();
|
|
244
|
-
this.dispatch("pin", { detail: { pinned } });
|
|
413
|
+
if (changed) this.dispatch("pin", { detail: { pinned } });
|
|
245
414
|
}
|
|
246
|
-
/** Mirrors the current `#pinned` onto the state hooks (clearing has-new once pinned). */
|
|
247
415
|
#reflectPinned() {
|
|
248
416
|
if (this.#pinned) {
|
|
249
417
|
this.element.setAttribute("data-pinned", "true");
|
|
@@ -252,10 +420,6 @@ var StickToBottomController = class extends Controller {
|
|
|
252
420
|
this.element.removeAttribute("data-pinned");
|
|
253
421
|
}
|
|
254
422
|
}
|
|
255
|
-
/**
|
|
256
|
-
* Mirrors whether content has arrived that the container did not follow onto the state
|
|
257
|
-
* hook and onto the regions declared for that state.
|
|
258
|
-
*/
|
|
259
423
|
#setHasNew(hasNew) {
|
|
260
424
|
this.#hasNew = hasNew;
|
|
261
425
|
if (hasNew) {
|
|
@@ -265,61 +429,31 @@ var StickToBottomController = class extends Controller {
|
|
|
265
429
|
}
|
|
266
430
|
this.#hasNewRegion.reflect(this.element, hasNew);
|
|
267
431
|
}
|
|
268
|
-
/** Whether the container currently sits within `threshold` of its bottom. */
|
|
269
432
|
#isPinned() {
|
|
270
433
|
if (!this.#measurable()) return false;
|
|
271
434
|
const el = this.element;
|
|
272
435
|
return el.scrollHeight - el.clientHeight - el.scrollTop <= this.#threshold;
|
|
273
436
|
}
|
|
274
|
-
/**
|
|
275
|
-
* The distance from the bottom that counts as pinned: a finite, non-negative number of
|
|
276
|
-
* pixels. Anything else names no distance the container can be at, and settles the
|
|
277
|
-
* comparison the same way at every scroll position, so it falls back to the default.
|
|
278
|
-
* `Number` reads `"abc"` as `NaN` and every comparison against it is false; a negative
|
|
279
|
-
* distance sits below the closest the container ever gets; `Infinity` is never
|
|
280
|
-
* exceeded. The first two stop following and flag every append as new, and the last
|
|
281
|
-
* never stops following — it takes the reading position the flag exists to protect.
|
|
282
|
-
* Zero is a real declaration: it pins at the exact bottom only.
|
|
283
|
-
*/
|
|
284
437
|
get #threshold() {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Whether the container has a box to scroll and to measure. One that is not rendered
|
|
290
|
-
* (inside a closed panel) reports every metric as 0, which reads as "already at the
|
|
291
|
-
* bottom" — a position describing no layout the user will ever see.
|
|
292
|
-
*/
|
|
438
|
+
return this.#safeThreshold;
|
|
439
|
+
}
|
|
293
440
|
#measurable() {
|
|
294
441
|
return this.element.clientHeight > 0;
|
|
295
442
|
}
|
|
296
|
-
/**
|
|
297
|
-
* Holds the pinned decision until the container is laid out — otherwise the panel opens
|
|
298
|
-
* at the top while the state claims the bottom, and the appends that arrived meanwhile
|
|
299
|
-
* were followed into a box that could not move rather than flagged.
|
|
300
|
-
*/
|
|
301
443
|
#waitForLayout() {
|
|
302
444
|
this.#awaitingLayout = true;
|
|
303
445
|
this.#layout.observe(this.element);
|
|
304
446
|
}
|
|
305
|
-
/** Runs the held decision once the container has the box it was waiting for. */
|
|
306
447
|
#onLaidOut() {
|
|
307
448
|
if (!this.#awaitingLayout || !this.#measurable()) return;
|
|
308
449
|
this.#stopWaitingForLayout();
|
|
309
450
|
if (this.pinOnConnectValue) this.#scrollToBottom("instant");
|
|
310
451
|
this.#updatePinned();
|
|
311
452
|
}
|
|
312
|
-
/** Releases the layout watch, whether or not the held decision ever ran. */
|
|
313
453
|
#stopWaitingForLayout() {
|
|
314
454
|
this.#awaitingLayout = false;
|
|
315
455
|
this.#layout.disconnect();
|
|
316
456
|
}
|
|
317
|
-
/**
|
|
318
|
-
* Scrolls to the bottom, clamped by the engine to the maximum scroll offset — which is
|
|
319
|
-
* 0 for a container tall enough to hold its whole content, so the jump moves nothing
|
|
320
|
-
* there. `behavior` defaults to the configured follow behavior; pass `"instant"` for a
|
|
321
|
-
* jump that must not animate.
|
|
322
|
-
*/
|
|
323
457
|
#scrollToBottom(behavior = this.#behavior()) {
|
|
324
458
|
const top = this.element.scrollHeight;
|
|
325
459
|
if (typeof this.element.scrollTo === "function") {
|
|
@@ -328,17 +462,6 @@ var StickToBottomController = class extends Controller {
|
|
|
328
462
|
this.element.scrollTop = top;
|
|
329
463
|
}
|
|
330
464
|
}
|
|
331
|
-
/**
|
|
332
|
-
* Points the append watch at the current `content` target, or at the container when
|
|
333
|
-
* there is none. Re-resolved whenever that target changes, so a swap does not leave the
|
|
334
|
-
* observer holding a detached node whose appends nobody sees.
|
|
335
|
-
*
|
|
336
|
-
* Stimulus runs the target callbacks outside the connected window too — before
|
|
337
|
-
* `connect()` for a target already in the DOM, and after `disconnect()` while the
|
|
338
|
-
* element is torn down — where this would arm an observer nothing releases. Re-syncing
|
|
339
|
-
* to the target already held is left alone, so an arrival still in flight is not
|
|
340
|
-
* dropped with the observer that was about to deliver it.
|
|
341
|
-
*/
|
|
342
465
|
#syncWatched() {
|
|
343
466
|
if (!this.#connected) return;
|
|
344
467
|
const next = this.hasContentTarget ? this.contentTarget : this.element;
|
|
@@ -349,24 +472,25 @@ var StickToBottomController = class extends Controller {
|
|
|
349
472
|
this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));
|
|
350
473
|
this.#observer.observe(next, { childList: true });
|
|
351
474
|
}
|
|
352
|
-
/** Releases the append watch and the element it held. */
|
|
353
475
|
#stopWatching() {
|
|
354
476
|
this.#observer?.disconnect();
|
|
355
477
|
this.#observer = null;
|
|
356
478
|
this.#watched = null;
|
|
357
479
|
}
|
|
358
|
-
/**
|
|
359
|
-
* The behavior a follow-scroll runs with. `"auto"` is **not** a request to arrive at
|
|
360
|
-
* once: it hands the decision to the element's computed `scroll-behavior`, so a
|
|
361
|
-
* consumer stylesheet saying `smooth` animates these scrolls too. Only `"instant"`
|
|
362
|
-
* overrides that CSS, which is why reduced motion and the `pinOnConnect` jump name it.
|
|
363
|
-
*/
|
|
364
480
|
#behavior() {
|
|
365
481
|
if (prefersReducedMotion()) return "instant";
|
|
366
482
|
return this.behaviorValue === "smooth" ? "smooth" : "auto";
|
|
367
483
|
}
|
|
484
|
+
get #safeThreshold() {
|
|
485
|
+
return this.#numbers.read(
|
|
486
|
+
this,
|
|
487
|
+
"threshold",
|
|
488
|
+
this.thresholdValue,
|
|
489
|
+
_StickToBottomController.values.threshold.default,
|
|
490
|
+
_StickToBottomController.valueConstraints.threshold
|
|
491
|
+
);
|
|
492
|
+
}
|
|
368
493
|
};
|
|
369
494
|
|
|
370
495
|
export { StickToBottomController };
|
|
371
|
-
//# sourceMappingURL=stick_to_bottom_controller.js.map
|
|
372
496
|
//# sourceMappingURL=stick_to_bottom_controller.js.map
|