Files
wehub-resource-sync 70bf21e064
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:11 +08:00

110 lines
2.8 KiB
TypeScript

import {
useContext,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import Frame from "./Frame";
import { DragContext } from "./SplitPane";
/**
* A wrapper around an iframe that loads any changes to the src in the background,
* keeping around the current content in the meantime. It does this by listening
* to the "load" event and swapping between two underlying iframe elements
*/
export function useRefreshableIframe(
src: string | undefined,
onLoad: (frame: HTMLIFrameElement) => void
) {
const isPaneDragging = useContext(DragContext);
const refs = [
useRef<HTMLIFrameElement>(null),
useRef<HTMLIFrameElement>(null),
];
const [index, setIndex] = useState(0);
const [isLoadingContent, setIsLoadingContent] = useState(false);
const [firstLoad, setFirstLoad] = useState(false);
useEffect(() => {
function onLoadEvent(this: HTMLIFrameElement) {
onLoad(this);
}
const first = refs[0].current;
const second = refs[1].current;
if (first && second) {
first.addEventListener("load", onLoadEvent);
second.addEventListener("load", onLoadEvent);
return () => {
first.removeEventListener("load", onLoadEvent);
second.removeEventListener("load", onLoadEvent);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable across renders and should not trigger re-subscription
}, [onLoad]);
function listen() {
if (!firstLoad) {
setFirstLoad(true);
}
requestAnimationFrame(() => {
setIndex(index === 0 ? 1 : 0);
setIsLoadingContent(false);
});
}
function setUrl(url: string) {
setIsLoadingContent(true);
const nextIndex = index === 0 ? 1 : 0;
const nextRef = refs[nextIndex].current;
if (nextRef) {
if (isLoadingContent) {
nextRef.removeEventListener("load", listen);
}
nextRef.addEventListener("load", listen, {
once: true,
});
nextRef.src = url;
}
}
useLayoutEffect(() => {
if (src) {
setUrl(src);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- setUrl depends on mutable state that would cause infinite loops if included
}, [src]);
const isLoading = isLoadingContent;
return {
firstLoad,
isLoading,
refresh() {
if (src) {
setUrl(src);
}
},
frame: (
<>
<Frame
innerRef={refs[0]}
style={{
opacity: index === 0 ? 1 : 0,
zIndex: index === 0 ? 10 : 5,
pointerEvents: isLoading || isPaneDragging ? "none" : "auto",
}}
sandbox="allow-forms allow-modals allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox"
/>
<Frame
innerRef={refs[1]}
style={{
opacity: index === 1 ? 1 : 0,
zIndex: index === 1 ? 10 : 5,
pointerEvents: isLoading || isPaneDragging ? "none" : "auto",
}}
sandbox="allow-forms allow-modals allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox"
/>
</>
),
};
}