Skip to content

Instantly share code, notes, and snippets.

@yano3nora
Last active July 15, 2023 09:34
Show Gist options
  • Save yano3nora/35f0a37eb4df31a78c168782ae852d31 to your computer and use it in GitHub Desktop.
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
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