mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-30 10:36:35 +02:00
4a88d6ff7a
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
173 lines
4.7 KiB
TypeScript
173 lines
4.7 KiB
TypeScript
import { defaultWindowId } from '@joplin/lib/reducer';
|
|
import shim from '@joplin/lib/shim';
|
|
import * as React from 'react';
|
|
import { useState, useEffect, useRef, createContext } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { SecondaryWindowApi } from '../utils/window/types';
|
|
|
|
// This component uses react-dom's Portals to render its children in a different HTML
|
|
// document. As children are rendered in a different Window/Document, they should avoid
|
|
// referencing the `window` and `document` globals. Instead, HTMLElement.ownerDocument
|
|
// and refs can be used to access the child component's DOM.
|
|
|
|
export const WindowIdContext = createContext(defaultWindowId);
|
|
|
|
type OnCloseCallback = ()=> void;
|
|
type OnFocusCallback = ()=> void;
|
|
|
|
export enum WindowMode {
|
|
Iframe, NewWindow,
|
|
}
|
|
|
|
interface Props {
|
|
// Note: children will be rendered in a different DOM from this node. Avoid using document.* methods
|
|
// in child components.
|
|
children: React.ReactNode[]|React.ReactNode;
|
|
title: string;
|
|
mode: WindowMode;
|
|
windowId: string;
|
|
onClose: OnCloseCallback;
|
|
onFocus?: OnFocusCallback;
|
|
}
|
|
|
|
const useDocument = (
|
|
mode: WindowMode,
|
|
iframeElement: HTMLIFrameElement|null,
|
|
onClose: OnCloseCallback,
|
|
) => {
|
|
const [doc, setDoc] = useState<Document>(null);
|
|
|
|
const onCloseRef = useRef(onClose);
|
|
onCloseRef.current = onClose;
|
|
|
|
useEffect(() => {
|
|
let openedWindow: Window|null = null;
|
|
const unmounted = false;
|
|
if (iframeElement) {
|
|
setDoc(iframeElement?.contentWindow?.document);
|
|
} else if (mode === WindowMode.NewWindow) {
|
|
openedWindow = window.open('about:blank');
|
|
setDoc(openedWindow.document);
|
|
|
|
// .onbeforeunload and .onclose events don't seem to fire when closed by a user -- rely on polling
|
|
// instead:
|
|
void (async () => {
|
|
while (!unmounted) {
|
|
await new Promise<void>(resolve => {
|
|
shim.setTimeout(() => resolve(), 2000);
|
|
});
|
|
|
|
if (openedWindow?.closed) {
|
|
onCloseRef.current?.();
|
|
openedWindow = null;
|
|
break;
|
|
}
|
|
}
|
|
})();
|
|
}
|
|
|
|
return () => {
|
|
// Delay: Closing immediately causes Electron to crash
|
|
setTimeout(() => {
|
|
if (!openedWindow?.closed) {
|
|
openedWindow?.close();
|
|
onCloseRef.current?.();
|
|
openedWindow = null;
|
|
}
|
|
}, 200);
|
|
|
|
if (iframeElement && !openedWindow) {
|
|
onCloseRef.current?.();
|
|
}
|
|
};
|
|
}, [iframeElement, mode]);
|
|
|
|
return doc;
|
|
};
|
|
|
|
type OnSetLoaded = (loaded: boolean)=> void;
|
|
const useDocumentSetup = (doc: Document|null, setLoaded: OnSetLoaded, onFocus?: OnFocusCallback) => {
|
|
const onFocusRef = useRef(onFocus);
|
|
onFocusRef.current = onFocus;
|
|
|
|
useEffect(() => {
|
|
if (!doc) return;
|
|
|
|
doc.open();
|
|
doc.write('<!DOCTYPE html><html><head></head><body></body></html>');
|
|
doc.close();
|
|
|
|
const cssUrls = [
|
|
'style.min.css',
|
|
];
|
|
|
|
for (const url of cssUrls) {
|
|
const style = doc.createElement('link');
|
|
style.rel = 'stylesheet';
|
|
style.href = url;
|
|
doc.head.appendChild(style);
|
|
}
|
|
|
|
const jsUrls = [
|
|
'vendor/lib/smalltalk/dist/smalltalk.min.js',
|
|
'./utils/window/eventHandlerOverrides.js',
|
|
];
|
|
for (const url of jsUrls) {
|
|
const script = doc.createElement('script');
|
|
script.src = url;
|
|
doc.head.appendChild(script);
|
|
}
|
|
|
|
doc.body.style.height = '100vh';
|
|
|
|
const containerWindow = doc.defaultView;
|
|
containerWindow.addEventListener('focus', () => {
|
|
onFocusRef.current?.();
|
|
});
|
|
if (doc.hasFocus()) {
|
|
onFocusRef.current?.();
|
|
}
|
|
|
|
setLoaded(true);
|
|
}, [doc, setLoaded]);
|
|
};
|
|
|
|
const NewWindowOrIFrame: React.FC<Props> = props => {
|
|
const [iframeRef, setIframeRef] = useState<HTMLIFrameElement|null>(null);
|
|
const [loaded, setLoaded] = useState(false);
|
|
|
|
const doc = useDocument(props.mode, iframeRef, props.onClose);
|
|
useDocumentSetup(doc, setLoaded, props.onFocus);
|
|
|
|
useEffect(() => {
|
|
if (!doc) return;
|
|
doc.title = props.title;
|
|
}, [doc, props.title]);
|
|
|
|
useEffect(() => {
|
|
const win = doc?.defaultView;
|
|
if (win && 'electronWindow' in win && typeof win.electronWindow === 'object') {
|
|
const electronWindow = win.electronWindow as SecondaryWindowApi;
|
|
electronWindow.onSetWindowId(props.windowId);
|
|
}
|
|
}, [doc, props.windowId]);
|
|
|
|
const parentNode = loaded ? doc?.body : null;
|
|
const wrappedChildren = <WindowIdContext.Provider value={props.windowId}>{props.children}</WindowIdContext.Provider>;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needed to allow adding the portal to the DOM
|
|
const contentPortal = parentNode && createPortal(wrappedChildren, parentNode) as any;
|
|
if (props.mode === WindowMode.NewWindow) {
|
|
return <div style={{ display: 'none' }}>{contentPortal}</div>;
|
|
} else {
|
|
return <iframe
|
|
ref={setIframeRef}
|
|
style={{ flexGrow: 1, width: '100%', height: '100%', border: 'none' }}
|
|
>
|
|
{contentPortal}
|
|
</iframe>;
|
|
}
|
|
};
|
|
|
|
export default NewWindowOrIFrame;
|