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,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/intersection_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/declared_value.ts
|
|
6
3
|
function parseDeclared(raw, parse, fallback) {
|
|
7
4
|
try {
|
|
8
5
|
return parse(raw);
|
|
@@ -10,8 +7,18 @@ function parseDeclared(raw, parse, fallback) {
|
|
|
10
7
|
return fallback;
|
|
11
8
|
}
|
|
12
9
|
}
|
|
10
|
+
function validSelector(element, raw, fallback) {
|
|
11
|
+
if (raw.length === 0) return fallback;
|
|
12
|
+
return parseDeclared(
|
|
13
|
+
raw,
|
|
14
|
+
(selector) => {
|
|
15
|
+
element.matches(selector);
|
|
16
|
+
return selector;
|
|
17
|
+
},
|
|
18
|
+
fallback
|
|
19
|
+
);
|
|
20
|
+
}
|
|
13
21
|
|
|
14
|
-
// src/utils/intersection_watcher.ts
|
|
15
22
|
function isBeforeRootStart(entry) {
|
|
16
23
|
const rect = entry.boundingClientRect;
|
|
17
24
|
if (rect.width === 0 && rect.height === 0) return false;
|
|
@@ -30,30 +37,12 @@ var IntersectionWatcher = class {
|
|
|
30
37
|
constructor(onEntries) {
|
|
31
38
|
this.#onEntries = onEntries;
|
|
32
39
|
}
|
|
33
|
-
/** Whether an observer is live (started, `IntersectionObserver` supported). */
|
|
34
40
|
get active() {
|
|
35
41
|
return this.#active;
|
|
36
42
|
}
|
|
37
|
-
/** Whether the live observer discarded configured options after construction failed. */
|
|
38
43
|
get usingPlatformDefaults() {
|
|
39
44
|
return this.#usingPlatformDefaults;
|
|
40
45
|
}
|
|
41
|
-
/**
|
|
42
|
-
* (Re)creates the observer and observes `targets`. Returns `false` — leaving
|
|
43
|
-
* the watcher inert — without `IntersectionObserver` support (very old
|
|
44
|
-
* browsers; the caller's no-JS fallback stays in charge) or with no targets.
|
|
45
|
-
* If initial construction with the configured options fails, the watcher
|
|
46
|
-
* warns and retries once with the same root and platform defaults. A
|
|
47
|
-
* `rootSelector` that does not parse resolves to the viewport (see
|
|
48
|
-
* {@link IntersectionWatchOptions.rootSelector}), so a typo never fails the
|
|
49
|
-
* call.
|
|
50
|
-
*
|
|
51
|
-
* @throws The fallback constructor error if both construction attempts fail,
|
|
52
|
-
* or whatever the platform throws from `observe()`. The exception is passed
|
|
53
|
-
* through unchanged, but the watcher rolls back first: every target observed
|
|
54
|
-
* so far is released and `active` stays `false`, so a caller that retries
|
|
55
|
-
* starts from a clean slate.
|
|
56
|
-
*/
|
|
57
46
|
start(targets, options = {}) {
|
|
58
47
|
this.stop();
|
|
59
48
|
if (typeof IntersectionObserver === "undefined") return false;
|
|
@@ -91,14 +80,6 @@ var IntersectionWatcher = class {
|
|
|
91
80
|
throw error;
|
|
92
81
|
}
|
|
93
82
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Re-delivers `target`'s CURRENT intersection state: `IntersectionObserver`
|
|
96
|
-
* only reports *changes*, but `observe()` always reports the present state,
|
|
97
|
-
* so unobserve→observe turns "still intersecting" into a fresh callback.
|
|
98
|
-
*
|
|
99
|
-
* @throws Whatever `unobserve()`/`observe()` throws. The watcher is stopped
|
|
100
|
-
* first, so it never stays live with a half-rearmed target.
|
|
101
|
-
*/
|
|
102
83
|
rearm(target) {
|
|
103
84
|
if (!this.#observer) return;
|
|
104
85
|
try {
|
|
@@ -109,7 +90,6 @@ var IntersectionWatcher = class {
|
|
|
109
90
|
throw error;
|
|
110
91
|
}
|
|
111
92
|
}
|
|
112
|
-
/** Severs the observer; late queued callbacks become no-ops via the guard. */
|
|
113
93
|
stop() {
|
|
114
94
|
this.#active = false;
|
|
115
95
|
this.#observer?.disconnect();
|
|
@@ -118,10 +98,85 @@ var IntersectionWatcher = class {
|
|
|
118
98
|
}
|
|
119
99
|
};
|
|
120
100
|
|
|
121
|
-
|
|
101
|
+
var MicrotaskCoalescer = class {
|
|
102
|
+
#run;
|
|
103
|
+
#queued = false;
|
|
104
|
+
#active = false;
|
|
105
|
+
#generation = 0;
|
|
106
|
+
constructor(run) {
|
|
107
|
+
this.#run = run;
|
|
108
|
+
}
|
|
109
|
+
activate() {
|
|
110
|
+
this.#active = true;
|
|
111
|
+
}
|
|
112
|
+
cancel() {
|
|
113
|
+
this.#active = false;
|
|
114
|
+
this.#queued = false;
|
|
115
|
+
this.#generation += 1;
|
|
116
|
+
}
|
|
117
|
+
get pending() {
|
|
118
|
+
return this.#queued;
|
|
119
|
+
}
|
|
120
|
+
schedule() {
|
|
121
|
+
if (!this.#active || this.#queued) return;
|
|
122
|
+
this.#queued = true;
|
|
123
|
+
const generation = this.#generation;
|
|
124
|
+
queueMicrotask(() => {
|
|
125
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
126
|
+
this.#queued = false;
|
|
127
|
+
this.#run();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
var NUMBER_BOUNDS = {
|
|
133
|
+
finite: { finite: true }};
|
|
134
|
+
function matchesNumberBounds(value, bounds) {
|
|
135
|
+
if (!Number.isFinite(value)) {
|
|
136
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
137
|
+
if (direction === null) return false;
|
|
138
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
139
|
+
}
|
|
140
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
141
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
142
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
143
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
144
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readNumber(raw, fallback, bounds) {
|
|
149
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var NumberValueReader = class {
|
|
153
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
154
|
+
read(owner, name, raw, fallback, bounds) {
|
|
155
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
156
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
157
|
+
this.#lastRejected.delete(name);
|
|
158
|
+
return resolved;
|
|
159
|
+
}
|
|
160
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
161
|
+
const literal = owner.element.getAttribute(attribute);
|
|
162
|
+
if (literal === null) {
|
|
163
|
+
this.#lastRejected.delete(name);
|
|
164
|
+
return resolved;
|
|
165
|
+
}
|
|
166
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
167
|
+
this.#lastRejected.set(name, literal);
|
|
168
|
+
console.warn(
|
|
169
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return resolved;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
122
176
|
var RATIO_PROPERTY = "--stimeo--intersection-ratio";
|
|
123
177
|
var RATIO_EPSILON = 0.01;
|
|
124
|
-
var IntersectionController = class extends Controller {
|
|
178
|
+
var IntersectionController = class _IntersectionController extends Controller {
|
|
179
|
+
#numbers = new NumberValueReader();
|
|
125
180
|
static values = {
|
|
126
181
|
threshold: { type: Number, default: 0 },
|
|
127
182
|
ratioSteps: { type: Number, default: 0 },
|
|
@@ -129,13 +184,18 @@ var IntersectionController = class extends Controller {
|
|
|
129
184
|
rootSelector: { type: String, default: "" },
|
|
130
185
|
once: { type: Boolean, default: false }
|
|
131
186
|
};
|
|
187
|
+
static valueConstraints = {
|
|
188
|
+
threshold: NUMBER_BOUNDS.finite,
|
|
189
|
+
ratioSteps: { finite: true, min: 0, max: 1e3 }
|
|
190
|
+
};
|
|
132
191
|
static actions = ["refresh"];
|
|
133
192
|
static events = ["enter", "exit", "change", "passed"];
|
|
134
|
-
/** Shared IO plumbing (support guard, root resolution, active guard, re-arm). */
|
|
135
193
|
#watcher = new IntersectionWatcher((entries) => this.#onIntersect(entries));
|
|
136
|
-
|
|
194
|
+
#rebuild = new MicrotaskCoalescer(() => this.#sync());
|
|
195
|
+
#rootSelector = "";
|
|
196
|
+
#builtRoot = null;
|
|
197
|
+
#builtOptions = "";
|
|
137
198
|
#effectiveThreshold = 0;
|
|
138
|
-
/** Bumped by `refresh()`: an in-flight batch becomes stale and stops. */
|
|
139
199
|
#generation = 0;
|
|
140
200
|
#onIntersect(entries) {
|
|
141
201
|
const generation = this.#generation;
|
|
@@ -151,46 +211,49 @@ var IntersectionController = class extends Controller {
|
|
|
151
211
|
}
|
|
152
212
|
}
|
|
153
213
|
connect() {
|
|
154
|
-
|
|
155
|
-
this.#
|
|
214
|
+
this.#rebuild.activate();
|
|
215
|
+
this.#sync();
|
|
156
216
|
}
|
|
157
217
|
disconnect() {
|
|
218
|
+
this.#rebuild.cancel();
|
|
158
219
|
this.#watcher.stop();
|
|
159
220
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Re-reads the visibility line and rebuilds the observer. Turbo 8 morphing
|
|
162
|
-
* rewrites the attribute in place without a reconnect, and the line is what
|
|
163
|
-
* the intersection callback compares every ratio against, so a value frozen at
|
|
164
|
-
* connect time would decide `data-intersecting` wrongly for the rest of the
|
|
165
|
-
* page's life. Nothing to rebuild before the first `connect()`; after a spent
|
|
166
|
-
* one-shot the watcher is deliberately stopped, and re-observing would deliver
|
|
167
|
-
* the current state and fire `enter` a second time.
|
|
168
|
-
*/
|
|
169
221
|
thresholdValueChanged() {
|
|
170
|
-
|
|
222
|
+
this.#rebuild.schedule();
|
|
223
|
+
}
|
|
224
|
+
ratioStepsValueChanged() {
|
|
225
|
+
this.#rebuild.schedule();
|
|
226
|
+
}
|
|
227
|
+
rootMarginValueChanged() {
|
|
228
|
+
this.#rebuild.schedule();
|
|
229
|
+
}
|
|
230
|
+
rootSelectorValueChanged() {
|
|
231
|
+
this.#rootSelector = validSelector(this.element, this.rootSelectorValue, "");
|
|
232
|
+
this.#rebuild.schedule();
|
|
171
233
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
234
|
+
onceValueChanged() {
|
|
235
|
+
this.#rebuild.schedule();
|
|
236
|
+
}
|
|
237
|
+
#sync() {
|
|
238
|
+
if (this.onceValue && this.element.getAttribute("data-intersecting") === "true") {
|
|
239
|
+
this.#watcher.stop();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const root = this.#rootSelector ? document.querySelector(this.#rootSelector) : null;
|
|
243
|
+
const threshold = this.#clampedThreshold();
|
|
244
|
+
const thresholds = this.#thresholds();
|
|
245
|
+
const options = `${this.rootMarginValue} ${threshold} ${thresholds.join(",")}`;
|
|
246
|
+
if (this.#watcher.active && root === this.#builtRoot && options === this.#builtOptions) return;
|
|
247
|
+
this.#builtRoot = root;
|
|
248
|
+
this.#builtOptions = options;
|
|
249
|
+
this.#effectiveThreshold = threshold;
|
|
175
250
|
this.#watcher.start(this.element, {
|
|
176
|
-
|
|
251
|
+
root,
|
|
177
252
|
rootMargin: this.rootMarginValue,
|
|
178
|
-
threshold:
|
|
253
|
+
threshold: thresholds
|
|
179
254
|
});
|
|
180
255
|
if (this.#watcher.usingPlatformDefaults) this.#effectiveThreshold = 0;
|
|
181
256
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Re-delivers the current intersection state as a fresh transition. Bound via
|
|
184
|
-
* `data-action` (e.g. `my-feed:appended@window->stimeo--intersection#refresh`).
|
|
185
|
-
*
|
|
186
|
-
* `IntersectionObserver` only reports state *changes*, so a sentinel that
|
|
187
|
-
* stays visible while content is appended below it never fires `enter` again
|
|
188
|
-
* and a hand-rolled infinite scroll stalls. `observe()` always delivers the
|
|
189
|
-
* current state, and clearing the recorded `data-intersecting`/`data-passed`
|
|
190
|
-
* makes that delivery count as a transition — a still-visible sentinel
|
|
191
|
-
* re-fires `enter`. No-op once the observer is gone (`once` fired, no
|
|
192
|
-
* `IntersectionObserver` support, or after `disconnect()`).
|
|
193
|
-
*/
|
|
194
257
|
refresh() {
|
|
195
258
|
if (!this.#watcher.active) return;
|
|
196
259
|
this.#generation += 1;
|
|
@@ -198,16 +261,6 @@ var IntersectionController = class extends Controller {
|
|
|
198
261
|
this.element.removeAttribute("data-passed");
|
|
199
262
|
this.#watcher.rearm(this.element);
|
|
200
263
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Reflects the visibility onto `data-intersecting` and fires `enter`/`exit`
|
|
203
|
-
* on transitions. The previous state is the DOM attribute (source of truth),
|
|
204
|
-
* so the observer's initial callback fires `enter` for an element that starts
|
|
205
|
-
* visible but stays silent after a cache restore that already recorded it.
|
|
206
|
-
* An initial not-visible state is established silently (no `exit`).
|
|
207
|
-
*
|
|
208
|
-
* @stimeoRuntimeOnly `once` decides whether this enter spends the watcher's one shot; the hook it
|
|
209
|
-
* writes follows the entry.
|
|
210
|
-
*/
|
|
211
264
|
#syncIntersecting(intersecting, ratio, entry) {
|
|
212
265
|
const previous = this.element.getAttribute("data-intersecting");
|
|
213
266
|
this.element.setAttribute("data-intersecting", intersecting ? "true" : "false");
|
|
@@ -220,57 +273,48 @@ var IntersectionController = class extends Controller {
|
|
|
220
273
|
});
|
|
221
274
|
}
|
|
222
275
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Which edge the element left across, for the `exit` detail. A non-zero
|
|
225
|
-
* `threshold` withdraws visibility while the element still overlaps the root,
|
|
226
|
-
* so the leaving rect can straddle the start edge — the direction is the
|
|
227
|
-
* element's own top against that edge, not whether it has cleared the root
|
|
228
|
-
* entirely (that is what `passed` reports). An element with no layout box
|
|
229
|
-
* (`display: none`, a collapsed `<details>`) is reported with an empty rect
|
|
230
|
-
* that carries no position at all, so it is deliberately neither direction
|
|
231
|
-
* and takes the "still ahead" reading.
|
|
232
|
-
*/
|
|
233
276
|
#leftViaStartEdge(entry) {
|
|
234
277
|
const rect = entry.boundingClientRect;
|
|
235
278
|
if (rect.width === 0 && rect.height === 0) return false;
|
|
236
279
|
return rect.top < (entry.rootBounds?.top ?? 0);
|
|
237
280
|
}
|
|
238
|
-
/**
|
|
239
|
-
* Reflects the "scrolled past" state onto `data-passed` and fires `passed` on
|
|
240
|
-
* transitions — the line sticky headers and reading progress key off. Like
|
|
241
|
-
* `enter`, an initial `passed=true` (page restored mid-scroll) fires; the
|
|
242
|
-
* initial `false` is established silently.
|
|
243
|
-
*/
|
|
244
281
|
#syncPassed(passed) {
|
|
245
282
|
const previous = this.element.getAttribute("data-passed");
|
|
246
283
|
this.element.setAttribute("data-passed", passed ? "true" : "false");
|
|
247
284
|
const changed = previous === null ? passed : previous === "true" !== passed;
|
|
248
285
|
if (changed) this.dispatch("passed", { detail: { passed } });
|
|
249
286
|
}
|
|
250
|
-
/** The configured `threshold`, clamped to the 0..1 the observer accepts. */
|
|
251
287
|
#clampedThreshold() {
|
|
252
|
-
return Math.min(1, Math.max(0, this
|
|
288
|
+
return Math.min(1, Math.max(0, this.#safeThreshold));
|
|
253
289
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Observer thresholds: the `threshold` line itself, plus `ratioSteps` evenly
|
|
256
|
-
* spaced steps when fine-grained `change` ratios are wanted (progress bars).
|
|
257
|
-
*
|
|
258
|
-
* 0 is always observed. An observer notifies only at the lines it was given,
|
|
259
|
-
* so a non-zero `threshold` on its own delivers its last callback while the
|
|
260
|
-
* element is still partly visible: the element leaving for good would never be
|
|
261
|
-
* reported, freezing the ratio and `data-passed` mid-departure.
|
|
262
|
-
*/
|
|
263
290
|
#thresholds() {
|
|
264
291
|
const thresholds = /* @__PURE__ */ new Set([0, this.#clampedThreshold()]);
|
|
265
|
-
if (this
|
|
266
|
-
for (let i = 0; i <= this
|
|
267
|
-
thresholds.add(i / this
|
|
292
|
+
if (this.#safeRatioSteps > 0) {
|
|
293
|
+
for (let i = 0; i <= this.#safeRatioSteps; i += 1) {
|
|
294
|
+
thresholds.add(i / this.#safeRatioSteps);
|
|
268
295
|
}
|
|
269
296
|
}
|
|
270
297
|
return [...thresholds].sort((a, b) => a - b);
|
|
271
298
|
}
|
|
299
|
+
get #safeThreshold() {
|
|
300
|
+
return this.#numbers.read(
|
|
301
|
+
this,
|
|
302
|
+
"threshold",
|
|
303
|
+
this.thresholdValue,
|
|
304
|
+
_IntersectionController.values.threshold.default,
|
|
305
|
+
_IntersectionController.valueConstraints.threshold
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
get #safeRatioSteps() {
|
|
309
|
+
return this.#numbers.read(
|
|
310
|
+
this,
|
|
311
|
+
"ratioSteps",
|
|
312
|
+
this.ratioStepsValue,
|
|
313
|
+
_IntersectionController.values.ratioSteps.default,
|
|
314
|
+
_IntersectionController.valueConstraints.ratioSteps
|
|
315
|
+
);
|
|
316
|
+
}
|
|
272
317
|
};
|
|
273
318
|
|
|
274
319
|
export { IntersectionController };
|
|
275
|
-
//# sourceMappingURL=intersection_controller.js.map
|
|
276
320
|
//# sourceMappingURL=intersection_controller.js.map
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/lazy_frame_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/declared_value.ts
|
|
6
3
|
function parseDeclared(raw, parse, fallback) {
|
|
7
4
|
try {
|
|
8
5
|
return parse(raw);
|
|
@@ -11,7 +8,6 @@ function parseDeclared(raw, parse, fallback) {
|
|
|
11
8
|
}
|
|
12
9
|
}
|
|
13
10
|
|
|
14
|
-
// src/utils/intersection_watcher.ts
|
|
15
11
|
function queryRoot(selector) {
|
|
16
12
|
if (!selector) return null;
|
|
17
13
|
return parseDeclared(selector, (raw) => document.querySelector(raw), null);
|
|
@@ -24,30 +20,12 @@ var IntersectionWatcher = class {
|
|
|
24
20
|
constructor(onEntries) {
|
|
25
21
|
this.#onEntries = onEntries;
|
|
26
22
|
}
|
|
27
|
-
/** Whether an observer is live (started, `IntersectionObserver` supported). */
|
|
28
23
|
get active() {
|
|
29
24
|
return this.#active;
|
|
30
25
|
}
|
|
31
|
-
/** Whether the live observer discarded configured options after construction failed. */
|
|
32
26
|
get usingPlatformDefaults() {
|
|
33
27
|
return this.#usingPlatformDefaults;
|
|
34
28
|
}
|
|
35
|
-
/**
|
|
36
|
-
* (Re)creates the observer and observes `targets`. Returns `false` — leaving
|
|
37
|
-
* the watcher inert — without `IntersectionObserver` support (very old
|
|
38
|
-
* browsers; the caller's no-JS fallback stays in charge) or with no targets.
|
|
39
|
-
* If initial construction with the configured options fails, the watcher
|
|
40
|
-
* warns and retries once with the same root and platform defaults. A
|
|
41
|
-
* `rootSelector` that does not parse resolves to the viewport (see
|
|
42
|
-
* {@link IntersectionWatchOptions.rootSelector}), so a typo never fails the
|
|
43
|
-
* call.
|
|
44
|
-
*
|
|
45
|
-
* @throws The fallback constructor error if both construction attempts fail,
|
|
46
|
-
* or whatever the platform throws from `observe()`. The exception is passed
|
|
47
|
-
* through unchanged, but the watcher rolls back first: every target observed
|
|
48
|
-
* so far is released and `active` stays `false`, so a caller that retries
|
|
49
|
-
* starts from a clean slate.
|
|
50
|
-
*/
|
|
51
29
|
start(targets, options = {}) {
|
|
52
30
|
this.stop();
|
|
53
31
|
if (typeof IntersectionObserver === "undefined") return false;
|
|
@@ -85,14 +63,6 @@ var IntersectionWatcher = class {
|
|
|
85
63
|
throw error;
|
|
86
64
|
}
|
|
87
65
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Re-delivers `target`'s CURRENT intersection state: `IntersectionObserver`
|
|
90
|
-
* only reports *changes*, but `observe()` always reports the present state,
|
|
91
|
-
* so unobserve→observe turns "still intersecting" into a fresh callback.
|
|
92
|
-
*
|
|
93
|
-
* @throws Whatever `unobserve()`/`observe()` throws. The watcher is stopped
|
|
94
|
-
* first, so it never stays live with a half-rearmed target.
|
|
95
|
-
*/
|
|
96
66
|
rearm(target) {
|
|
97
67
|
if (!this.#observer) return;
|
|
98
68
|
try {
|
|
@@ -103,7 +73,6 @@ var IntersectionWatcher = class {
|
|
|
103
73
|
throw error;
|
|
104
74
|
}
|
|
105
75
|
}
|
|
106
|
-
/** Severs the observer; late queued callbacks become no-ops via the guard. */
|
|
107
76
|
stop() {
|
|
108
77
|
this.#active = false;
|
|
109
78
|
this.#observer?.disconnect();
|
|
@@ -112,7 +81,17 @@ var IntersectionWatcher = class {
|
|
|
112
81
|
}
|
|
113
82
|
};
|
|
114
83
|
|
|
115
|
-
|
|
84
|
+
var ListenerSet = class {
|
|
85
|
+
#abort = new AbortController();
|
|
86
|
+
add(target, type, handler, options) {
|
|
87
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
88
|
+
}
|
|
89
|
+
dispose() {
|
|
90
|
+
this.#abort.abort();
|
|
91
|
+
this.#abort = new AbortController();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
116
95
|
var LazyFrameController = class extends Controller {
|
|
117
96
|
static values = {
|
|
118
97
|
url: { type: String, default: "" },
|
|
@@ -120,38 +99,29 @@ var LazyFrameController = class extends Controller {
|
|
|
120
99
|
once: { type: Boolean, default: true }
|
|
121
100
|
};
|
|
122
101
|
static events = ["load"];
|
|
123
|
-
/** Shared IO plumbing (support guard, active guard, teardown). */
|
|
124
102
|
#watcher = new IntersectionWatcher((entries) => this.#onIntersect(entries));
|
|
125
103
|
#connected = false;
|
|
126
104
|
#loaded = false;
|
|
127
|
-
/**
|
|
128
|
-
* Whether the observer last saw the frame inside the observed area — `null`
|
|
129
|
-
* until it has reported. A re-entry needs a frame that was seen inside and
|
|
130
|
-
* then left, so an unknown position is never a departure.
|
|
131
|
-
*/
|
|
132
105
|
#inside = null;
|
|
133
|
-
/**
|
|
134
|
-
* Focus reaching the frame starts the FIRST load before it intersects (keyboard / AT).
|
|
135
|
-
* `#load` releases this listener, so focus never reaches an already-loaded frame.
|
|
136
|
-
*/
|
|
137
106
|
#onFocus = () => this.#load();
|
|
138
|
-
|
|
139
|
-
* Follows the held URL at runtime: one that arrives arms the frame, one that is
|
|
140
|
-
* taken away disarms it, so an empty `url` holds no triggers either way.
|
|
141
|
-
*
|
|
142
|
-
* Stimulus runs value callbacks before `connect()`, so the connected guard keeps
|
|
143
|
-
* arming in one place. A frame that already loaded keeps its held URL for the next
|
|
144
|
-
* re-entry instead of fetching on the spot.
|
|
145
|
-
*/
|
|
107
|
+
#focusListeners = new ListenerSet();
|
|
146
108
|
urlValueChanged() {
|
|
147
109
|
if (!this.#connected || this.#loaded) return;
|
|
148
110
|
if (this.urlValue) this.#arm();
|
|
149
111
|
else this.#stopObserving();
|
|
150
112
|
}
|
|
151
|
-
/** Rebuilds the observer when the early-load margin changes at runtime. */
|
|
152
113
|
rootMarginValueChanged() {
|
|
153
114
|
if (this.#connected && this.#watcher.active) this.#observe();
|
|
154
115
|
}
|
|
116
|
+
onceValueChanged() {
|
|
117
|
+
if (!this.#connected || !this.#loaded) return;
|
|
118
|
+
if (this.onceValue) {
|
|
119
|
+
this.#watcher.stop();
|
|
120
|
+
} else if (!this.#watcher.active) {
|
|
121
|
+
this.#inside = null;
|
|
122
|
+
this.#arm();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
155
125
|
connect() {
|
|
156
126
|
this.#loaded = this.element.hasAttribute("data-lazy-loaded");
|
|
157
127
|
this.#inside = null;
|
|
@@ -163,10 +133,9 @@ var LazyFrameController = class extends Controller {
|
|
|
163
133
|
this.#connected = false;
|
|
164
134
|
this.#stopObserving();
|
|
165
135
|
}
|
|
166
|
-
/** Attaches the triggers that still have a job to do. */
|
|
167
136
|
#arm() {
|
|
168
137
|
if (!this.urlValue) return;
|
|
169
|
-
if (!this.#loaded) this.element
|
|
138
|
+
if (!this.#loaded) this.#focusListeners.add(this.element, "focusin", this.#onFocus);
|
|
170
139
|
this.#observe();
|
|
171
140
|
}
|
|
172
141
|
#observe() {
|
|
@@ -183,7 +152,6 @@ var LazyFrameController = class extends Controller {
|
|
|
183
152
|
}
|
|
184
153
|
}
|
|
185
154
|
}
|
|
186
|
-
/** Applies an intersecting entry: the first one loads, a return re-fetches. */
|
|
187
155
|
#enter(returning) {
|
|
188
156
|
if (!this.#loaded) {
|
|
189
157
|
this.#load();
|
|
@@ -192,26 +160,15 @@ var LazyFrameController = class extends Controller {
|
|
|
192
160
|
if (this.onceValue || !returning) return;
|
|
193
161
|
this.#reload();
|
|
194
162
|
}
|
|
195
|
-
/**
|
|
196
|
-
* Starts the load by writing the held URL to `src`.
|
|
197
|
-
*
|
|
198
|
-
* @stimeoRuntimeOnly `url` is the address this one load fetches and `once` decides whether it
|
|
199
|
-
* stops the watcher.
|
|
200
|
-
*/
|
|
201
163
|
#load() {
|
|
202
164
|
if (!this.urlValue) return;
|
|
203
165
|
this.#loaded = true;
|
|
204
166
|
this.element.setAttribute("src", this.urlValue);
|
|
205
167
|
this.element.setAttribute("data-lazy-loaded", "true");
|
|
206
168
|
this.dispatch("load", { detail: { url: this.urlValue } });
|
|
207
|
-
this.
|
|
169
|
+
this.#focusListeners.dispose();
|
|
208
170
|
if (this.onceValue) this.#watcher.stop();
|
|
209
171
|
}
|
|
210
|
-
/**
|
|
211
|
-
* Re-entry while `once` is off: fetch the held URL again.
|
|
212
|
-
*
|
|
213
|
-
* @stimeoRuntimeOnly `url` is the address this one re-entry reloads.
|
|
214
|
-
*/
|
|
215
172
|
#reload() {
|
|
216
173
|
const src = this.element.getAttribute("src");
|
|
217
174
|
if (this.urlValue && this.urlValue !== src) {
|
|
@@ -226,10 +183,9 @@ var LazyFrameController = class extends Controller {
|
|
|
226
183
|
}
|
|
227
184
|
#stopObserving() {
|
|
228
185
|
this.#watcher.stop();
|
|
229
|
-
this.
|
|
186
|
+
this.#focusListeners.dispose();
|
|
230
187
|
}
|
|
231
188
|
};
|
|
232
189
|
|
|
233
190
|
export { LazyFrameController };
|
|
234
|
-
//# sourceMappingURL=lazy_frame_controller.js.map
|
|
235
191
|
//# sourceMappingURL=lazy_frame_controller.js.map
|