148 lines
5.2 KiB
JavaScript
148 lines
5.2 KiB
JavaScript
// Exposes window.__CLONE_LIST_SECTIONS__() which returns a list of candidate top-level
|
|
// section bounding boxes for the capture script's scroll planner. We emit one entry per
|
|
// large flex/block child of <body>, <main>, or any element classed as section/container/etc.
|
|
// The capture script uses these to scroll-to-section instead of fixed-pixel stepping.
|
|
(() => {
|
|
const SECTIONLIKE_TAGS = new Set(['section', 'header', 'footer', 'nav', 'main', 'article', 'aside']);
|
|
const SECTIONLIKE_CLASS_RE =
|
|
/(^|\s)(section|hero|container|banner|elementor-section|elementor-element-[^\s]+|e-con|e-parent|wp-block-[^\s]+)(\s|$)/i;
|
|
|
|
function isViewportWide(el, vw) {
|
|
const r = el.getBoundingClientRect();
|
|
return r.width >= vw * 0.5 && r.height >= 80;
|
|
}
|
|
|
|
function isVisible(el) {
|
|
const cs = getComputedStyle(el);
|
|
if (cs.display === 'none' || cs.visibility === 'hidden') return false;
|
|
if (parseFloat(cs.opacity) === 0) return false;
|
|
return true;
|
|
}
|
|
|
|
function looksLikeSection(el) {
|
|
const tag = el.tagName.toLowerCase();
|
|
if (SECTIONLIKE_TAGS.has(tag)) return true;
|
|
if (el.id && /(hero|banner|section|footer|nav|header)/i.test(el.id)) return true;
|
|
if (typeof el.className === 'string' && SECTIONLIKE_CLASS_RE.test(el.className)) return true;
|
|
return false;
|
|
}
|
|
|
|
function cssPath(el) {
|
|
if (!el || el.nodeType !== 1) return null;
|
|
if (el.id) return `#${el.id}`;
|
|
const parts = [];
|
|
while (el && el.nodeType === 1 && parts.length < 8) {
|
|
let s = el.tagName.toLowerCase();
|
|
const p = el.parentNode;
|
|
if (p && p.nodeType === 1) {
|
|
const sibs = Array.from(p.children).filter((c) => c.tagName === el.tagName);
|
|
if (sibs.length > 1) s += `:nth-of-type(${sibs.indexOf(el) + 1})`;
|
|
}
|
|
parts.unshift(s);
|
|
el = el.parentNode;
|
|
}
|
|
return parts.join(' > ');
|
|
}
|
|
|
|
const SECTION_QUERY =
|
|
'section, header, footer, nav, main, article, ' +
|
|
'[class*="section"], [class*="elementor-element"], [class*="e-parent"], [class*="e-con"], ' +
|
|
'[class*="hero"], [class*="banner"], [id*="section"], [id*="hero"], [id*="banner"]';
|
|
|
|
function snapshotEntry(el, vw) {
|
|
const r = el.getBoundingClientRect();
|
|
return {
|
|
element: el,
|
|
selector: cssPath(el),
|
|
tag: el.tagName.toLowerCase(),
|
|
id: el.id || null,
|
|
className: typeof el.className === 'string' ? el.className.slice(0, 200) : null,
|
|
x: r.x,
|
|
y: r.y + window.scrollY,
|
|
width: r.width,
|
|
height: r.height,
|
|
looksLikeSection: looksLikeSection(el),
|
|
};
|
|
}
|
|
|
|
function collectInScope(scope, vw, out, candidates) {
|
|
for (const el of candidates) {
|
|
if (!scope.contains(el) || el === scope) continue;
|
|
if (!isVisible(el) || !isViewportWide(el, vw)) continue;
|
|
// Outermost-wins inside this scope
|
|
let skip = false;
|
|
for (const o of out) {
|
|
if (o.element.contains(el)) {
|
|
skip = true;
|
|
break;
|
|
}
|
|
}
|
|
if (skip) continue;
|
|
for (let i = out.length - 1; i >= 0; i--) {
|
|
if (el.contains(out[i].element)) out.splice(i, 1);
|
|
}
|
|
out.push(snapshotEntry(el, vw));
|
|
}
|
|
}
|
|
|
|
window.__CLONE_LIST_SECTIONS__ = () => {
|
|
const vw = window.innerWidth;
|
|
const vh = window.innerHeight;
|
|
const docHeight = Math.max(document.documentElement.scrollHeight, document.body ? document.body.scrollHeight : 0);
|
|
|
|
// Pass 1: known section-like elements
|
|
const candidates = Array.from(document.querySelectorAll(SECTION_QUERY));
|
|
|
|
// Pass 2: large direct children of body/main as a fallback
|
|
const root = document.querySelector('main') || document.body;
|
|
if (root) {
|
|
for (const child of root.children) {
|
|
if (!candidates.includes(child)) candidates.push(child);
|
|
}
|
|
}
|
|
|
|
// First-pass dedup with outermost-wins
|
|
const out = [];
|
|
for (const el of candidates) {
|
|
if (!isVisible(el) || !isViewportWide(el, vw)) continue;
|
|
let skip = false;
|
|
for (const o of out) {
|
|
if (o.element.contains(el)) {
|
|
skip = true;
|
|
break;
|
|
}
|
|
}
|
|
if (skip) continue;
|
|
for (let i = out.length - 1; i >= 0; i--) {
|
|
if (el.contains(out[i].element)) out.splice(i, 1);
|
|
}
|
|
out.push(snapshotEntry(el, vw));
|
|
}
|
|
|
|
// Pass 3: oversized-wrapper expansion. WordPress / Elementor sites commonly
|
|
// wrap the whole page in <main id="main">. Without this step the outer
|
|
// wrapper wins and individual sections are dropped, so the smart-scroll
|
|
// loop never visits them and lazy backgrounds never fire.
|
|
const oversizedThreshold = Math.max(vh * 2, docHeight * 0.5);
|
|
const expanded = [];
|
|
for (const sec of out) {
|
|
if (sec.height < oversizedThreshold) {
|
|
expanded.push(sec);
|
|
continue;
|
|
}
|
|
const inner = [];
|
|
const innerCandidates = Array.from(sec.element.querySelectorAll(SECTION_QUERY));
|
|
collectInScope(sec.element, vw, inner, innerCandidates);
|
|
// Only replace the wrapper if recursion produced enough sections to be
|
|
// meaningful — otherwise this is a genuine large section, not a wrapper.
|
|
if (inner.length >= 3) expanded.push(...inner);
|
|
else expanded.push(sec);
|
|
}
|
|
|
|
return expanded
|
|
.filter((s) => s.height > 80)
|
|
.sort((a, b) => a.y - b.y)
|
|
.map(({ element, ...rest }) => rest);
|
|
};
|
|
})();
|