From 9ddf3c8e2d2b251b24a1e8c99f9a9ccc91bea5ef Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:00:43 -0400 Subject: [PATCH] Improve blog embed resize robustness --- js/blog/index.js | 206 ++++++++++++++++++++++++++++++++++++++++++----- views/blog.html | 3 + 2 files changed, 189 insertions(+), 20 deletions(-) diff --git a/js/blog/index.js b/js/blog/index.js index 56016962..725c7641 100644 --- a/js/blog/index.js +++ b/js/blog/index.js @@ -153,10 +153,48 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { return '*'; })(); + let destroyed = false; + registerCleanup(() => { + destroyed = true; + }); + + const parseCssNumber = (value) => { + if (typeof value !== 'string') { + return 0; + } + const numeric = Number.parseFloat(value); + return Number.isFinite(numeric) ? numeric : 0; + }; + + const computeMarginAdjustment = (node) => { + if (!node || typeof window.getComputedStyle !== 'function') { + return 0; + } + let topMargin = 0; + let bottomMargin = 0; + try { + const first = node.firstElementChild; + const last = node.lastElementChild; + if (first) { + topMargin = parseCssNumber(window.getComputedStyle(first).marginTop); + } + if (last) { + bottomMargin = parseCssNumber(window.getComputedStyle(last).marginBottom); + } + } catch (error) { + userLogger.error('[blog] failed to compute margin adjustment for resize messaging', error); + } + const adjustment = Math.max(0, topMargin) + Math.max(0, bottomMargin); + return Number.isFinite(adjustment) ? adjustment : 0; + }; + let lastHeight = null; let pendingFrame = null; const measureAndPost = (force = false) => { + if (destroyed) { + return; + } const docElement = document.documentElement; const body = document.body; @@ -175,18 +213,18 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { if (!node) { return; } - addCandidate(node.scrollHeight); - addCandidate(node.offsetHeight); - addCandidate(node.clientHeight); + const marginAdjustment = computeMarginAdjustment(node); + addCandidate(node.scrollHeight + marginAdjustment); + addCandidate(node.offsetHeight + marginAdjustment); + addCandidate(node.clientHeight + marginAdjustment); + try { + const rect = node.getBoundingClientRect(); + addCandidate(rect.height + marginAdjustment); + } catch (error) { + userLogger.error('[blog] failed to compute element rect for resize messaging', error); + } }; - try { - const rect = element.getBoundingClientRect(); - addCandidate(rect.height); - } catch (error) { - userLogger.error('[blog] failed to compute embed rect for resize messaging', error); - } - addElementMetrics(element); if (body && body !== element) { addElementMetrics(body); @@ -194,6 +232,9 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { if (docElement && docElement !== element && docElement !== body) { addElementMetrics(docElement); } + if (document.scrollingElement && document.scrollingElement !== body && document.scrollingElement !== docElement) { + addElementMetrics(document.scrollingElement); + } const height = candidates.length > 0 ? Math.max(...candidates) : null; if (!Number.isFinite(height)) { @@ -211,6 +252,9 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { }; const queueMeasurement = (force = false) => { + if (destroyed) { + return; + } if (force) { measureAndPost(true); return; @@ -257,19 +301,22 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { if (document.documentElement) { observeNode(observer, document.documentElement); } + if (document.scrollingElement) { + observeNode(observer, document.scrollingElement); + } registerCleanup(() => { observer.disconnect(); observedNodes.clear(); }); - } else { - const intervalId = window.setInterval(() => { - queueMeasurement(); - }, 500); - registerCleanup(() => { - window.clearInterval(intervalId); - }); } + const pollingIntervalId = window.setInterval(() => { + queueMeasurement(); + }, 750); + registerCleanup(() => { + window.clearInterval(pollingIntervalId); + }); + const mutationTargets = [element]; if (document.body && document.body !== element) { mutationTargets.push(document.body); @@ -277,16 +324,85 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { if (document.documentElement && document.documentElement !== element && document.documentElement !== document.body) { mutationTargets.push(document.documentElement); } + if ( + document.scrollingElement && + document.scrollingElement !== element && + document.scrollingElement !== document.body && + document.scrollingElement !== document.documentElement + ) { + mutationTargets.push(document.scrollingElement); + } + + const trackedMediaNodes = new WeakSet(); + const MEDIA_SELECTOR = 'img, picture, video, audio, iframe, object, embed'; + + const watchMediaNode = (node) => { + if (!(node instanceof Element)) { + return; + } + const candidates = []; + if (node.matches(MEDIA_SELECTOR)) { + candidates.push(node); + } + candidates.push(...node.querySelectorAll(MEDIA_SELECTOR)); + candidates.forEach((media) => { + if (trackedMediaNodes.has(media)) { + return; + } + trackedMediaNodes.add(media); + const handleMediaEvent = () => { + queueMeasurement(true); + }; + const mediaEvents = ['load', 'loadeddata', 'loadedmetadata', 'readystatechange', 'error']; + mediaEvents.forEach((eventName) => { + media.addEventListener(eventName, handleMediaEvent); + }); + registerCleanup(() => { + mediaEvents.forEach((eventName) => { + media.removeEventListener(eventName, handleMediaEvent); + }); + }); + if ( + (media instanceof HTMLImageElement && media.complete) || + (media instanceof HTMLVideoElement && media.readyState >= 1) || + (media instanceof HTMLAudioElement && media.readyState >= 1) || + (media instanceof HTMLIFrameElement && media.contentWindow) + ) { + queueMeasurement(true); + } + }); + }; + + watchMediaNode(element); if (typeof MutationObserver === 'function' && mutationTargets.length > 0) { - const mutationObserver = new MutationObserver(() => { - queueMeasurement(); + const mutationObserver = new MutationObserver((mutations) => { + let shouldMeasure = false; + mutations.forEach((mutation) => { + for (const node of mutation.addedNodes) { + watchMediaNode(node); + } + if (mutation.type === 'attributes') { + watchMediaNode(mutation.target); + } + if ( + mutation.type === 'childList' || + mutation.type === 'attributes' || + mutation.type === 'characterData' + ) { + shouldMeasure = true; + } + }); + if (shouldMeasure) { + queueMeasurement(); + } }); mutationTargets.forEach((target) => { mutationObserver.observe(target, { childList: true, subtree: true, - attributes: false, + attributes: true, + characterData: true, }); }); registerCleanup(() => { @@ -319,6 +435,24 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { window.removeEventListener('resize', handleResize); }); + const handleOrientationChange = () => { + queueMeasurement(true); + }; + window.addEventListener('orientationchange', handleOrientationChange); + registerCleanup(() => { + window.removeEventListener('orientationchange', handleOrientationChange); + }); + + const handleTransitionEnd = () => { + queueMeasurement(); + }; + element.addEventListener('transitionend', handleTransitionEnd, true); + element.addEventListener('animationend', handleTransitionEnd, true); + registerCleanup(() => { + element.removeEventListener('transitionend', handleTransitionEnd, true); + element.removeEventListener('animationend', handleTransitionEnd, true); + }); + const handleLoad = () => { queueMeasurement(true); }; @@ -326,6 +460,38 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { registerCleanup(() => { window.removeEventListener('load', handleLoad); }); + + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + queueMeasurement(true); + } + }; + document.addEventListener('visibilitychange', handleVisibilityChange); + registerCleanup(() => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }); + + if (document.fonts && typeof document.fonts.addEventListener === 'function') { + const handleFontLoad = () => { + queueMeasurement(true); + }; + document.fonts.addEventListener('loadingdone', handleFontLoad); + document.fonts.addEventListener('loadingerror', handleFontLoad); + registerCleanup(() => { + document.fonts.removeEventListener('loadingdone', handleFontLoad); + document.fonts.removeEventListener('loadingerror', handleFontLoad); + }); + } + + if (document.fonts?.ready && typeof document.fonts.ready.then === 'function') { + document.fonts.ready + .then(() => { + queueMeasurement(true); + }) + .catch(() => { + queueMeasurement(true); + }); + } } function formatDate(unixSeconds, { includeTime = false } = {}) { diff --git a/views/blog.html b/views/blog.html index 0d5448db..50af79a6 100644 --- a/views/blog.html +++ b/views/blog.html @@ -102,6 +102,9 @@ }; iframe.addEventListener('load', () => { + receivedFirstHeight = false; + stopRequestLoop(); + hasStarted = false; runInitialRequests(); }, { signal });