文件
mynode/packages/app-desktop/gui/NewWindowOrIFrame.tsx
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

178 行
4.8 KiB
TypeScript

import { defaultWindowId } from '@joplin/lib/reducer';
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 OnSetWindowCallback = (window: Window)=> 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;
onWindow: OnSetWindowCallback;
}
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;
let unmounted = false;
if (iframeElement) {
setDoc(iframeElement?.contentWindow?.document);
} else if (mode === WindowMode.NewWindow) {
openedWindow = window.open('about:blank');
// Required to support TinyMCE:
openedWindow.document.open();
openedWindow.document.write('<!DOCTYPE html><html><head></head><body></body></html>');
openedWindow.document.close();
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 => {
setTimeout(() => resolve(), 2000);
});
// Re-check after sleep to avoid duplicate WINDOW_CLOSE if IPC already fired.
if (unmounted) break;
if (openedWindow?.closed) {
// Null out doc first so React stops rendering into the destroyed window
// before WINDOW_CLOSE triggers unmounting (prevents renderer crash on Windows).
setDoc(null);
openedWindow = null;
break;
}
}
})();
}
return () => {
unmounted = true;
// 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) => {
useEffect(() => {
if (!doc) return;
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';
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);
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 onWindowRef = useRef(props.onWindow);
onWindowRef.current = props.onWindow;
useEffect(() => {
const win = doc?.defaultView;
if (win && onWindowRef.current) {
onWindowRef.current(win);
}
}, [doc]);
const parentNode = loaded ? doc?.body : null;
const wrappedChildren = <WindowIdContext.Provider value={props.windowId}>{props.children}</WindowIdContext.Provider>;
const contentPortal = parentNode && createPortal(wrappedChildren, parentNode);
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;