use-companion-window.tsx
Nov 23, 2024ยทuse document-in-picture api via react hook
import { useState, useEffect, useRef } from "react";
import { Logger } from "./logger";
const logger = Logger.createChild("useCompanionWindow");
interface CompanionWindow {
enter: () => Promise<void>;
exit: () => void;
isActive: boolean;
isSupported: boolean;
}
export function useCompanionWindow(ref: React.RefObject<HTMLElement | null>): CompanionWindow {
const [companionWindow, setCompanionWindow] = useState<Window | null>(null);
const originalParent = useRef<ParentNode | null>(null);
const placeholder = useRef<Comment | null>(null);
// Check if the Document Picture-in-Picture API is supported
const isSupported = typeof window.documentPictureInPicture !== "undefined";
const enter = async () => {
if (!isSupported) {
// No operation if API is not supported
return;
}
if (!ref.current) {
logger.error("The provided ref is not attached to an element.");
return;
}
try {
// get ref dimensions
const { width, height } = ref.current.getBoundingClientRect();
// Request a companion window
const companionWindow = await window.documentPictureInPicture?.requestWindow({
width,
height,
});
if (!companionWindow) {
return
}
setCompanionWindow(companionWindow ?? null);
// Copy stylesheets to the companion window
[...document.styleSheets].forEach((styleSheet) => {
try {
const cssRules = [...styleSheet.cssRules]
.map((rule) => rule.cssText)
.join("");
const style = document.createElement("style");
style.textContent = cssRules;
companionWindow.document.head.appendChild(style);
} catch (e) {
const link: HTMLLinkElement = document.createElement("link");
link.rel = "stylesheet";
link.type = styleSheet.type;
if (styleSheet.media) {
link.media = styleSheet.media.mediaText;
}
if (styleSheet.href) {
link.href = styleSheet.href;
}
companionWindow.document.head.appendChild(link);
}
});
// Save the original parent and create a placeholder
originalParent.current = ref.current.parentNode;
placeholder.current = document.createComment(
"Companion window placeholder"
);
originalParent.current?.replaceChild(placeholder.current, ref.current);
// Move the element into the companion window
companionWindow.document.body.appendChild(ref.current);
// Handle the companion window unload event
companionWindow.addEventListener("unload", () => {
if (originalParent.current && ref.current && placeholder.current) {
originalParent.current.replaceChild(ref.current, placeholder.current);
}
setCompanionWindow(null);
});
} catch (error) {
logger.error("Failed to open companion window:", error);
}
};
const exit = () => {
if (!isSupported) {
// No operation if API is not supported
return;
}
if (companionWindow) {
// Move the element back to the original document
if (originalParent.current && ref.current && placeholder.current) {
originalParent.current.replaceChild(ref.current, placeholder.current);
}
companionWindow.close();
setCompanionWindow(null);
}
};
useEffect(() => {
// Clean up when the component unmounts
return () => {
if (companionWindow) {
exit();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [companionWindow]);
if (!ref) {
return {
enter: () => Promise.resolve(),
exit: () => {},
isActive: false,
isSupported: false,
};
}
return {
enter,
exit,
isActive: !!companionWindow,
isSupported,
};
}