@base44/vite-plugin 1.0.15 → 1.0.17
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.
- package/dist/injections/canvas-wheel-zoom-bridge.d.ts +1 -1
- package/dist/injections/canvas-wheel-zoom-bridge.d.ts.map +1 -1
- package/dist/injections/canvas-wheel-zoom-bridge.js +53 -16
- package/dist/injections/canvas-wheel-zoom-bridge.js.map +1 -1
- package/dist/injections/page-height-bridge.d.ts.map +1 -1
- package/dist/injections/page-height-bridge.js +215 -27
- package/dist/injections/page-height-bridge.js.map +1 -1
- package/dist/injections/visual-edit-agent.d.ts.map +1 -1
- package/dist/injections/visual-edit-agent.js +1 -0
- package/dist/injections/visual-edit-agent.js.map +1 -1
- package/dist/statics/index.mjs +7 -7
- package/dist/statics/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/injections/canvas-wheel-zoom-bridge.ts +67 -16
- package/src/injections/page-height-bridge.ts +229 -29
- package/src/injections/visual-edit-agent.ts +1 -0
package/package.json
CHANGED
|
@@ -1,27 +1,78 @@
|
|
|
1
|
+
import { PLUGIN_ELEMENT_ATTR } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
type CanvasWheelPanData = {
|
|
4
|
+
deltaX: number;
|
|
5
|
+
deltaY: number;
|
|
6
|
+
deltaMode: number;
|
|
7
|
+
clientX: number;
|
|
8
|
+
clientY: number;
|
|
9
|
+
shiftKey: boolean;
|
|
10
|
+
ctrlKey: false;
|
|
11
|
+
metaKey: false;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function elementFromEventTarget(target: EventTarget | null): Element | null {
|
|
15
|
+
if (target instanceof Element) return target;
|
|
16
|
+
if (target instanceof Node) return target.parentElement;
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isPluginOwnedTarget(target: EventTarget | null): boolean {
|
|
21
|
+
return elementFromEventTarget(target)?.closest(`[${PLUGIN_ELEMENT_ATTR}]`) != null;
|
|
22
|
+
}
|
|
23
|
+
|
|
1
24
|
export function createCanvasWheelZoomBridgeController() {
|
|
25
|
+
let isEnabled = false;
|
|
26
|
+
|
|
2
27
|
const onWheel = (event: WheelEvent): void => {
|
|
3
|
-
if (
|
|
28
|
+
if (isPluginOwnedTarget(event.target)) return;
|
|
4
29
|
|
|
5
30
|
event.preventDefault();
|
|
31
|
+
if (event.ctrlKey || event.metaKey) {
|
|
32
|
+
window.parent.postMessage({
|
|
33
|
+
type: "canvas-wheel-zoom",
|
|
34
|
+
data: {
|
|
35
|
+
deltaY: event.deltaY,
|
|
36
|
+
deltaMode: event.deltaMode,
|
|
37
|
+
clientX: event.clientX,
|
|
38
|
+
clientY: event.clientY,
|
|
39
|
+
ctrlKey: event.ctrlKey,
|
|
40
|
+
metaKey: event.metaKey,
|
|
41
|
+
},
|
|
42
|
+
}, "*");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const panData: CanvasWheelPanData = {
|
|
47
|
+
deltaX: event.deltaX,
|
|
48
|
+
deltaY: event.deltaY,
|
|
49
|
+
deltaMode: event.deltaMode,
|
|
50
|
+
clientX: event.clientX,
|
|
51
|
+
clientY: event.clientY,
|
|
52
|
+
shiftKey: event.shiftKey,
|
|
53
|
+
ctrlKey: false,
|
|
54
|
+
metaKey: false,
|
|
55
|
+
};
|
|
6
56
|
window.parent.postMessage({
|
|
7
|
-
type: "canvas-wheel-
|
|
8
|
-
data:
|
|
9
|
-
deltaY: event.deltaY,
|
|
10
|
-
deltaMode: event.deltaMode,
|
|
11
|
-
clientX: event.clientX,
|
|
12
|
-
clientY: event.clientY,
|
|
13
|
-
ctrlKey: event.ctrlKey,
|
|
14
|
-
metaKey: event.metaKey,
|
|
15
|
-
},
|
|
57
|
+
type: "canvas-wheel-pan",
|
|
58
|
+
data: panData,
|
|
16
59
|
}, "*");
|
|
17
60
|
};
|
|
18
61
|
|
|
62
|
+
const enable = (): void => {
|
|
63
|
+
if (isEnabled) return;
|
|
64
|
+
isEnabled = true;
|
|
65
|
+
window.addEventListener("wheel", onWheel, { capture: true, passive: false });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const disable = (): void => {
|
|
69
|
+
if (!isEnabled) return;
|
|
70
|
+
isEnabled = false;
|
|
71
|
+
window.removeEventListener("wheel", onWheel, true);
|
|
72
|
+
};
|
|
73
|
+
|
|
19
74
|
return {
|
|
20
|
-
enable
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
teardown: (): void => {
|
|
24
|
-
window.removeEventListener("wheel", onWheel, true);
|
|
25
|
-
},
|
|
75
|
+
enable,
|
|
76
|
+
disable,
|
|
26
77
|
};
|
|
27
78
|
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
// fire on their own.
|
|
4
4
|
//
|
|
5
5
|
// parent → child { type: "freeze-vh-units", referenceVhBase?: number }
|
|
6
|
-
// Rewrites every `vh` in
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// Rewrites every viewport-height unit (`vh`/`dvh`/`svh`/`lvh`) in
|
|
7
|
+
// <style> + CSSOM to a CSS variable-backed expression (kills the
|
|
8
|
+
// vh ↔ auto-resize feedback loop). Repeated calls update the variable,
|
|
9
|
+
// so callers can rebase without recovering raw CSS. Idempotent; covers
|
|
10
|
+
// HMR-added styles too. Fire-and-forget.
|
|
10
11
|
//
|
|
11
12
|
// parent → child { type: "measure-page-height", settleMs?: number }
|
|
12
13
|
// After `settleMs` (default 2000) posts the page's measured content height
|
|
@@ -35,6 +36,19 @@ const FALLBACK_VHBASE: number = 900;
|
|
|
35
36
|
const MIN_VHBASE: number = 400;
|
|
36
37
|
const DEFAULT_SETTLE_MS: number = 2000;
|
|
37
38
|
const NEUTRALIZE_DEBOUNCE_MS: number = 16;
|
|
39
|
+
// Three consecutive same-height samples ≈ ~50ms of "no change". Three is
|
|
40
|
+
// enough to filter single-frame jitter from layout passes but short enough
|
|
41
|
+
// that responses arrive promptly when the DOM is already settled.
|
|
42
|
+
const STABILITY_SAMPLES: number = 3;
|
|
43
|
+
// Interval between stability samples. ~1 frame at 60Hz; chosen as a plain
|
|
44
|
+
// setTimeout (not rAF) because jsdom rAFs are unreliably timed for tests and
|
|
45
|
+
// in real browsers a 16ms tick is post-layout for any layout work that fits
|
|
46
|
+
// inside one frame.
|
|
47
|
+
const STABILITY_TICK_MS: number = 16;
|
|
48
|
+
// Hard cap on the stability poll past settleMs. Pages that genuinely never
|
|
49
|
+
// stabilize (looping height-animating content) reply with their last sample
|
|
50
|
+
// rather than hanging the parent's resize logic.
|
|
51
|
+
const STABILITY_MAX_WAIT_MS: number = 1500;
|
|
38
52
|
const REFERENCE_VH_BASE_VAR: string = "--base44-reference-vh-base";
|
|
39
53
|
|
|
40
54
|
const noop: () => void = (): void => {};
|
|
@@ -91,9 +105,21 @@ export function setupPageHeightBridge(): () => void {
|
|
|
91
105
|
export function createPageHeightBridgeController(): PageHeightBridgeController {
|
|
92
106
|
let vhCleanups: Array<() => void> | null = null;
|
|
93
107
|
let vhForceRun: (() => void) | null = null;
|
|
94
|
-
let
|
|
108
|
+
let pendingSettle: number | undefined;
|
|
109
|
+
let pendingTick: number | undefined;
|
|
95
110
|
let pendingOrigin: string = "*";
|
|
96
111
|
|
|
112
|
+
const cancelPending = (): void => {
|
|
113
|
+
if (pendingSettle !== undefined) {
|
|
114
|
+
window.clearTimeout(pendingSettle);
|
|
115
|
+
pendingSettle = undefined;
|
|
116
|
+
}
|
|
117
|
+
if (pendingTick !== undefined) {
|
|
118
|
+
window.clearTimeout(pendingTick);
|
|
119
|
+
pendingTick = undefined;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
97
123
|
return {
|
|
98
124
|
freezeVhUnits: (override: number | undefined): void => {
|
|
99
125
|
const referenceVhBase: number = resolveReferenceVhBase(override);
|
|
@@ -109,14 +135,52 @@ export function createPageHeightBridgeController(): PageHeightBridgeController {
|
|
|
109
135
|
// Target the requester's origin so the height isn't broadcast to anyone
|
|
110
136
|
// who happens to embed us. Falls back to "*" when origin is unavailable
|
|
111
137
|
// (jsdom default, sandboxed iframes with `null` origin).
|
|
138
|
+
//
|
|
139
|
+
// Stability-wait response: after `settleMs`, poll measureContentHeight at
|
|
140
|
+
// STABILITY_TICK_MS intervals until it's unchanged for STABILITY_SAMPLES
|
|
141
|
+
// consecutive ticks, then send exactly one reply. Catches late React
|
|
142
|
+
// mounts (sticky-positioned sections committing after settleMs), debounced
|
|
143
|
+
// neutralizer mutations not yet flushed, image/font-driven layout shifts.
|
|
144
|
+
// STABILITY_MAX_WAIT_MS caps the poll so a page that genuinely never
|
|
145
|
+
// settles still replies eventually.
|
|
112
146
|
measurePageHeight: (origin: string, settleMs: number = DEFAULT_SETTLE_MS): void => {
|
|
113
147
|
pendingOrigin = origin;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
148
|
+
cancelPending();
|
|
149
|
+
|
|
150
|
+
pendingSettle = window.setTimeout((): void => {
|
|
151
|
+
pendingSettle = undefined;
|
|
152
|
+
// Flush any debounced unscroll / inline-vh rewrites NOW so the first
|
|
153
|
+
// sample sees a DOM that reflects every mutation triggered during the
|
|
154
|
+
// settle window. Without this, an `overflow-y: scroll` container that
|
|
155
|
+
// mounted mid-settle can still be hiding its children at sample time.
|
|
156
|
+
vhForceRun?.();
|
|
157
|
+
|
|
158
|
+
const deadline: number = nowMs() + STABILITY_MAX_WAIT_MS;
|
|
159
|
+
let lastHeight: number = -1;
|
|
160
|
+
let stableSamples: number = 0;
|
|
161
|
+
|
|
162
|
+
const respond = (height: number): void => {
|
|
163
|
+
pendingTick = undefined;
|
|
118
164
|
window.parent.postMessage({ type: "page-height-measured", height }, pendingOrigin);
|
|
119
|
-
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const tick = (): void => {
|
|
168
|
+
pendingTick = undefined;
|
|
169
|
+
const height: number = measureContentHeight();
|
|
170
|
+
if (height === lastHeight) {
|
|
171
|
+
stableSamples++;
|
|
172
|
+
} else {
|
|
173
|
+
stableSamples = 1;
|
|
174
|
+
lastHeight = height;
|
|
175
|
+
}
|
|
176
|
+
if (stableSamples >= STABILITY_SAMPLES || nowMs() >= deadline) {
|
|
177
|
+
respond(height);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
pendingTick = window.setTimeout(tick, STABILITY_TICK_MS);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
tick();
|
|
120
184
|
}, settleMs);
|
|
121
185
|
},
|
|
122
186
|
|
|
@@ -126,11 +190,18 @@ export function createPageHeightBridgeController(): PageHeightBridgeController {
|
|
|
126
190
|
vhCleanups = null;
|
|
127
191
|
vhForceRun = null;
|
|
128
192
|
}
|
|
129
|
-
|
|
193
|
+
cancelPending();
|
|
130
194
|
},
|
|
131
195
|
};
|
|
132
196
|
}
|
|
133
197
|
|
|
198
|
+
function nowMs(): number {
|
|
199
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
200
|
+
return performance.now();
|
|
201
|
+
}
|
|
202
|
+
return Date.now();
|
|
203
|
+
}
|
|
204
|
+
|
|
134
205
|
function resolveReferenceVhBase(override: number | undefined): number {
|
|
135
206
|
if (override !== undefined) return override;
|
|
136
207
|
const detected: number = window.innerHeight || 0;
|
|
@@ -147,18 +218,65 @@ function setReferenceVhBase(referenceVhBase: number): void {
|
|
|
147
218
|
// parent requests (idempotent — already-rewritten text is skipped via the
|
|
148
219
|
// processed sets).
|
|
149
220
|
function startVhNeutralizer(cleanups: Array<() => void>): () => void {
|
|
150
|
-
|
|
221
|
+
// Match vh + the dynamic/small/large variants. Tailwind v4 emits `h-screen`
|
|
222
|
+
// as `100dvh`; all four collapse to the same frozen `referenceVhBase`.
|
|
223
|
+
const VH_RE: RegExp = /(\d+(?:\.\d+)?)(?:d|s|l)?vh\b/g;
|
|
151
224
|
const processedStyles: WeakSet<HTMLStyleElement> = new WeakSet();
|
|
152
225
|
const processedSheets: WeakMap<CSSStyleSheet, number> = new WeakMap();
|
|
153
226
|
const watchedStyles: WeakSet<HTMLStyleElement> = new WeakSet();
|
|
154
227
|
const styleObservers: Set<MutationObserver> = new Set();
|
|
155
228
|
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
229
|
+
const unscrolled: WeakSet<Element> = new WeakSet();
|
|
230
|
+
|
|
231
|
+
const rewrite = (input: string): string =>
|
|
232
|
+
input.replace(VH_RE, (_match: string, n: string): string =>
|
|
233
|
+
`calc(var(${REFERENCE_VH_BASE_VAR}) * ${formatVhFactor(n)})`,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Defeats internal vertical scrollers. The canvas wants the whole page laid
|
|
237
|
+
// out top-to-bottom, but `<div class="overflow-y-auto" style={{height:
|
|
238
|
+
// "100vh"}}>` hides its children behind an internal scrollbar — only the
|
|
239
|
+
// first child appears in the preview. Forcing `overflow-y: visible` lets
|
|
240
|
+
// children paint outside the parent's box; block flow positions them at
|
|
241
|
+
// their natural offsets so document.body extends to include them. `auto`
|
|
242
|
+
// and `scroll` only — `hidden` and `clip` express intentional clipping (UI
|
|
243
|
+
// chrome, rounded-corner masks) we shouldn't undo.
|
|
244
|
+
const unscrollY = (el: Element): void => {
|
|
245
|
+
if (unscrolled.has(el)) return;
|
|
246
|
+
const computedStyle: CSSStyleDeclaration = window.getComputedStyle(el);
|
|
247
|
+
const ovY: string = computedStyle.overflowY;
|
|
248
|
+
if (ovY !== "auto" && ovY !== "scroll") return;
|
|
249
|
+
unscrolled.add(el);
|
|
250
|
+
(el as HTMLElement).style.setProperty("overflow-y", "visible", "important");
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const unscrollSubtree = (root: Element): void => {
|
|
254
|
+
unscrollY(root);
|
|
255
|
+
root.querySelectorAll<HTMLElement>("*").forEach(unscrollY);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Rewrites vh-bearing properties on a single element's inline style. Covers
|
|
259
|
+
// React's `style={{ height: "100vh" }}` and imperative `el.style.h = ".vh"`
|
|
260
|
+
// — both bypass <style> tags and CSSOM. Per-property setProperty preserves
|
|
261
|
+
// `!important`. Snapshotting prop names first guards against iteration-time
|
|
262
|
+
// mutation of `style.length`.
|
|
263
|
+
const rewriteInlineStyle = (el: Element): void => {
|
|
264
|
+
const style: CSSStyleDeclaration | undefined = (el as HTMLElement).style;
|
|
265
|
+
if (!style || style.length === 0) return;
|
|
266
|
+
const props: string[] = [];
|
|
267
|
+
for (let i: number = 0; i < style.length; i++) {
|
|
268
|
+
const prop: string | undefined = style[i];
|
|
269
|
+
if (prop) props.push(prop);
|
|
270
|
+
}
|
|
271
|
+
for (const prop of props) {
|
|
272
|
+
const value: string = style.getPropertyValue(prop);
|
|
273
|
+
if (!value || value.indexOf("vh") === -1) continue;
|
|
274
|
+
const next: string = rewrite(value);
|
|
275
|
+
if (next !== value) style.setProperty(prop, next, style.getPropertyPriority(prop));
|
|
276
|
+
}
|
|
277
|
+
};
|
|
161
278
|
|
|
279
|
+
const neutralize = (): void => {
|
|
162
280
|
document.querySelectorAll<HTMLStyleElement>("style").forEach((el: HTMLStyleElement): void => {
|
|
163
281
|
watchStyleEl(el);
|
|
164
282
|
if (processedStyles.has(el)) return;
|
|
@@ -182,6 +300,15 @@ function startVhNeutralizer(cleanups: Array<() => void>): () => void {
|
|
|
182
300
|
rewriteVhInRules(rules, rewrite);
|
|
183
301
|
processedSheets.set(sheet, rules.length);
|
|
184
302
|
}
|
|
303
|
+
|
|
304
|
+
// Initial sweep of inline `style="...vh..."` attributes already in the DOM
|
|
305
|
+
// at freeze time. Future inline-style mutations (React commits, motion
|
|
306
|
+
// libraries, imperative assignments) are picked up by inlineStyleObserver.
|
|
307
|
+
document.querySelectorAll<HTMLElement>('[style*="vh"]').forEach(rewriteInlineStyle);
|
|
308
|
+
|
|
309
|
+
// Defeat internal scrollers — same dual coverage (initial sweep here,
|
|
310
|
+
// subtree additions in inlineStyleObserver).
|
|
311
|
+
if (document.body) unscrollSubtree(document.body);
|
|
185
312
|
};
|
|
186
313
|
|
|
187
314
|
const debouncer: Debouncer = createDebouncer(neutralize, NEUTRALIZE_DEBOUNCE_MS);
|
|
@@ -231,6 +358,58 @@ function startVhNeutralizer(cleanups: Array<() => void>): () => void {
|
|
|
231
358
|
cleanups.push((): void => document.removeEventListener("DOMContentLoaded", attachHeadObserver));
|
|
232
359
|
}
|
|
233
360
|
|
|
361
|
+
// Pass 3: inline `style` attributes. One global observer covers every
|
|
362
|
+
// element — past, present, and future — without per-node tracking. Two
|
|
363
|
+
// mutation kinds matter:
|
|
364
|
+
// • attributes: React/JS sets `style` on an element already in the tree
|
|
365
|
+
// (`el.style.h = "..vh"`, re-render diff). After rewrite the value has
|
|
366
|
+
// no "vh", so the next mutation gates out — single-tick convergence.
|
|
367
|
+
// • childList: a node is mounted with its `style` attribute already set
|
|
368
|
+
// off-tree (React's initial mount path uses createElement+setAttribute
|
|
369
|
+
// BEFORE appendChild, so attribute mutations never fire for them). Scan
|
|
370
|
+
// the added subtree for `[style*="vh"]` and rewrite.
|
|
371
|
+
// Microtask delivery means rewrites land before layout, so no flash.
|
|
372
|
+
const scanSubtreeForInlineVh = (node: Node): void => {
|
|
373
|
+
if (!(node instanceof Element)) return;
|
|
374
|
+
const attr: string | null = node.getAttribute("style");
|
|
375
|
+
if (attr && attr.indexOf("vh") !== -1) rewriteInlineStyle(node);
|
|
376
|
+
node.querySelectorAll<HTMLElement>('[style*="vh"]').forEach(rewriteInlineStyle);
|
|
377
|
+
// New subtrees may introduce overflow-y: auto|scroll containers (route
|
|
378
|
+
// changes, conditional UI). Catch them here too.
|
|
379
|
+
unscrollSubtree(node);
|
|
380
|
+
};
|
|
381
|
+
const inlineStyleObserver: MutationObserver = new MutationObserver(
|
|
382
|
+
(mutations: MutationRecord[]): void => {
|
|
383
|
+
for (const m of mutations) {
|
|
384
|
+
if (m.type === "attributes") {
|
|
385
|
+
const target: Node = m.target;
|
|
386
|
+
if (!(target instanceof Element)) continue;
|
|
387
|
+
const attr: string | null = target.getAttribute("style");
|
|
388
|
+
if (!attr || attr.indexOf("vh") === -1) continue;
|
|
389
|
+
rewriteInlineStyle(target);
|
|
390
|
+
} else if (m.type === "childList") {
|
|
391
|
+
for (let i: number = 0; i < m.addedNodes.length; i++) {
|
|
392
|
+
const node: Node | undefined = m.addedNodes[i];
|
|
393
|
+
if (node) scanSubtreeForInlineVh(node);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
cleanups.push((): void => inlineStyleObserver.disconnect());
|
|
400
|
+
|
|
401
|
+
const attachInlineStyleObserver = (): void => {
|
|
402
|
+
const root: Element | null = document.documentElement;
|
|
403
|
+
if (!root) return;
|
|
404
|
+
inlineStyleObserver.observe(root, {
|
|
405
|
+
attributes: true,
|
|
406
|
+
attributeFilter: ["style"],
|
|
407
|
+
childList: true,
|
|
408
|
+
subtree: true,
|
|
409
|
+
});
|
|
410
|
+
};
|
|
411
|
+
attachInlineStyleObserver();
|
|
412
|
+
|
|
234
413
|
return debouncedNeutralize;
|
|
235
414
|
}
|
|
236
415
|
|
|
@@ -269,13 +448,17 @@ function measureContentHeight(): number {
|
|
|
269
448
|
document.documentElement.scrollHeight,
|
|
270
449
|
document.body?.scrollHeight ?? 0,
|
|
271
450
|
);
|
|
451
|
+
const referenceVhBase: number = readReferenceVhBase();
|
|
452
|
+
// Excludes `body.clientHeight`: it grows with content, which makes
|
|
453
|
+
// viewportBottom land at the document bottom and trips the stretched-
|
|
454
|
+
// container heuristic on the last section. `referenceVhBase` keeps the
|
|
455
|
+
// h-screen-wrapper case covered when the iframe is shorter than 100vh.
|
|
272
456
|
const viewportHeight: number = Math.max(
|
|
273
457
|
window.innerHeight || 0,
|
|
274
458
|
document.documentElement.clientHeight,
|
|
275
|
-
|
|
459
|
+
referenceVhBase,
|
|
276
460
|
);
|
|
277
461
|
const contentBottom: number = measureElementContentBottom(viewportHeight);
|
|
278
|
-
const referenceVhBase: number = readReferenceVhBase();
|
|
279
462
|
if (contentBottom > 0) return Math.ceil(Math.max(contentBottom, referenceVhBase));
|
|
280
463
|
return Math.ceil(Math.max(scrollHeight, referenceVhBase));
|
|
281
464
|
}
|
|
@@ -296,10 +479,15 @@ function measureElementContentBottom(viewportHeight: number): number {
|
|
|
296
479
|
const el: Element | undefined = elements[i];
|
|
297
480
|
if (!el) continue;
|
|
298
481
|
const childContentBottom: number = readChildrenContentBottom(el, contentBottoms);
|
|
299
|
-
const
|
|
300
|
-
const selfBottom: number = isViewportStretchedContainer(
|
|
482
|
+
const metrics: ElementMetrics = readElementMetrics(el);
|
|
483
|
+
const selfBottom: number = isViewportStretchedContainer(
|
|
484
|
+
metrics,
|
|
485
|
+
childContentBottom,
|
|
486
|
+
viewportHeight,
|
|
487
|
+
viewportBottom,
|
|
488
|
+
)
|
|
301
489
|
? 0
|
|
302
|
-
:
|
|
490
|
+
: metrics.bottom;
|
|
303
491
|
contentBottoms.set(el, Math.max(childContentBottom, selfBottom));
|
|
304
492
|
}
|
|
305
493
|
|
|
@@ -319,12 +507,17 @@ function readChildrenContentBottom(
|
|
|
319
507
|
return childContentBottom;
|
|
320
508
|
}
|
|
321
509
|
|
|
322
|
-
|
|
510
|
+
type ElementMetrics = { bottom: number; height: number };
|
|
511
|
+
|
|
512
|
+
function readElementMetrics(el: Element): ElementMetrics {
|
|
323
513
|
const computedStyle: CSSStyleDeclaration = window.getComputedStyle(el);
|
|
324
|
-
if (isOutOfFlowDecoration(computedStyle)) return 0;
|
|
514
|
+
if (isOutOfFlowDecoration(computedStyle)) return { bottom: 0, height: 0 };
|
|
325
515
|
const rect: DOMRect = el.getBoundingClientRect();
|
|
326
|
-
if (rect.width === 0 && rect.height === 0) return 0;
|
|
327
|
-
return
|
|
516
|
+
if (rect.width === 0 && rect.height === 0) return { bottom: 0, height: 0 };
|
|
517
|
+
return {
|
|
518
|
+
bottom: rect.bottom + window.scrollY + readMarginBottom(computedStyle),
|
|
519
|
+
height: rect.height,
|
|
520
|
+
};
|
|
328
521
|
}
|
|
329
522
|
|
|
330
523
|
function readMarginBottom(computedStyle: CSSStyleDeclaration): number {
|
|
@@ -337,15 +530,22 @@ function isOutOfFlowDecoration(computedStyle: CSSStyleDeclaration): boolean {
|
|
|
337
530
|
return computedStyle.position === "absolute" && computedStyle.pointerEvents === "none";
|
|
338
531
|
}
|
|
339
532
|
|
|
533
|
+
// A "stretched container" spans the full viewport top-to-bottom. Requires
|
|
534
|
+
// BOTH `bottom ≈ viewportBottom` AND `height ≈ viewportHeight` — otherwise an
|
|
535
|
+
// in-flow section that just happens to end at viewportBottom (e.g. when the
|
|
536
|
+
// iframe has been content-sized to the previous measurement) gets falsely
|
|
537
|
+
// filtered, undermeasuring the page.
|
|
340
538
|
function isViewportStretchedContainer(
|
|
341
|
-
|
|
539
|
+
metrics: ElementMetrics,
|
|
342
540
|
childBottom: number,
|
|
541
|
+
viewportHeight: number,
|
|
343
542
|
viewportBottom: number,
|
|
344
543
|
): boolean {
|
|
345
544
|
return (
|
|
346
545
|
childBottom > 0 &&
|
|
347
|
-
Math.abs(
|
|
348
|
-
|
|
546
|
+
Math.abs(metrics.bottom - viewportBottom) <= 1 &&
|
|
547
|
+
Math.abs(metrics.height - viewportHeight) <= 1 &&
|
|
548
|
+
metrics.bottom - childBottom > 8
|
|
349
549
|
);
|
|
350
550
|
}
|
|
351
551
|
|