Last active
July 15, 2023 09:34
-
-
Save yano3nora/35f0a37eb4df31a78c168782ae852d31 to your computer and use it in GitHub Desktop.
phaser-portal-window - Phaser x React x window.open by createPortal. #js #react #phaser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Phaser from 'phaser' | |
import { ReactNode, useEffect, useRef, useState } from 'react' | |
import { createPortal } from 'react-dom' | |
const PHASER_CONFIG = { /* ... */ } | |
const WIDTH = 1920 / 2 | |
const HEIGHT = 1080 / 2 | |
const LEFT = (screen.width - WIDTH) / 2 | |
const TOP = (screen.height - HEIGHT) / 2 | |
const copyStyles = (sourceDoc: Document, targetDoc: Document) => { | |
Array.from(sourceDoc.querySelectorAll('link[rel="stylesheet"], style')) | |
.forEach(link => { | |
targetDoc.head.appendChild(link.cloneNode(true)) | |
}) | |
} | |
/** | |
* @link https://github.com/facebook/react/issues/12355 | |
* @example | |
* import { Button, useDisclosure } from '@chakra-ui/react' | |
* | |
* const { isOpen, onOpen, onClose } = useDisclosure() | |
* | |
* return <> | |
* <Button onClick={onOpen} isDisabled={isOpen} /> | |
* { | |
* isOpen && | |
* <PhaserPortalWindow onClose={onClose}> | |
* <> | |
* <SomeComponent /> | |
* <img src={window.origin + '/image.png'} /> | |
* </> | |
* </PhaserPortalWindow> | |
* } | |
* </> | |
*/ | |
export const PhaserPortalWindow = ({ children, onClose }: { | |
children: ReactNode | |
onClose?: () => void | |
}) => { | |
const phaserRef = useRef<Phaser.Game>() | |
const styleObserver = useRef<MutationObserver>() | |
const [externalWindow, setExternalWindow] = useState<Window | null>(null) | |
const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null) | |
const cleanup = () => { | |
phaserRef.current?.destroy(true) | |
styleObserver.current?.disconnect() | |
externalWindow?.close() | |
onClose && onClose() | |
} | |
/** | |
* setup new window | |
*/ | |
useEffect(() => { | |
const containerElement = document.createElement('div') | |
const externalWindow = window.open( | |
'', | |
'', | |
// https://bugs.chromium.org/p/chromium/issues/detail?id=137681 | |
`width=${WIDTH},height=${HEIGHT},top=${TOP},left=${LEFT}`, | |
)! | |
phaserRef.current = new Phaser.Game({ | |
...PHASER_CONFIG, | |
parent: containerElement, | |
}) | |
externalWindow.document.body.style.backgroundColor = 'black' | |
externalWindow.document.body.oncontextmenu = e => e.preventDefault() | |
// append the element to the external document before setting ref | |
// so that React could detect event bindding correctly | |
// https://github.com/facebook/react/issues/12355 | |
externalWindow.document.body.appendChild(containerElement) | |
externalWindow.onunload = cleanup | |
window.addEventListener('beforeunload', cleanup) | |
setContainerRef(containerElement) | |
setExternalWindow(externalWindow) | |
return cleanup | |
}, []) | |
/** | |
* copy & sync styles | |
*/ | |
useEffect(() => { | |
if (!externalWindow) { | |
return | |
} | |
copyStyles(document, externalWindow.document) | |
if (styleObserver.current) { | |
return | |
} | |
styleObserver.current = new MutationObserver(mutationsList => { | |
for (const mutation of mutationsList) { | |
if (mutation.type === 'childList') { | |
for (const node of mutation.addedNodes) { | |
if (node.nodeName === 'STYLE') { | |
externalWindow.document.head.appendChild(node.cloneNode(true)) | |
} | |
} | |
} | |
} | |
}) | |
styleObserver.current.observe(document.head, { childList: true }) | |
}, [externalWindow]) | |
return ( | |
containerRef && | |
phaserRef.current | |
) | |
? createPortal(children, phaserRef.current?.domContainer) | |
: null | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment