一些检查失败了
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
155 行
4.1 KiB
TypeScript
155 行
4.1 KiB
TypeScript
import * as React from 'react';
|
|
import { useRef, useImperativeHandle, forwardRef, useEffect, useMemo, useContext, useCallback } from 'react';
|
|
import useViewIsReady from './hooks/useViewIsReady';
|
|
import useThemeCss from './hooks/useThemeCss';
|
|
import useContentSize from './hooks/useContentSize';
|
|
import useHtmlLoader from './hooks/useHtmlLoader';
|
|
import useWebviewToPluginMessages from './hooks/useWebviewToPluginMessages';
|
|
import useScriptLoader from './hooks/useScriptLoader';
|
|
import Logger from '@joplin/utils/Logger';
|
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
|
import { WindowIdContext } from '../../gui/NewWindowOrIFrame';
|
|
import useSubmitHandler from './hooks/useSubmitHandler';
|
|
import useFormData from './hooks/useFormData';
|
|
import Setting from '@joplin/lib/models/Setting';
|
|
import getAssetPath from '../../utils/getAssetPath';
|
|
import { toForwardSlashes } from '@joplin/utils/path';
|
|
|
|
const logger = Logger.create('UserWebview');
|
|
|
|
export interface Props {
|
|
html: string;
|
|
scripts: string[];
|
|
pluginId: string;
|
|
viewId: string;
|
|
themeId: number;
|
|
minWidth?: number;
|
|
minHeight?: number;
|
|
fitToContent?: boolean;
|
|
borderBottom?: boolean;
|
|
theme?: Record<string, unknown>;
|
|
onSubmit?: ()=> void;
|
|
onDismiss?: ()=> void;
|
|
onReady?: ()=> void;
|
|
}
|
|
|
|
export interface UserWebviewRef {
|
|
formData: ()=> unknown;
|
|
focus: ()=> void;
|
|
}
|
|
|
|
function UserWebview(props: Props, ref: React.Ref<UserWebviewRef>) {
|
|
const minWidth = props.minWidth ? props.minWidth : 200;
|
|
const minHeight = props.minHeight ? props.minHeight : 20;
|
|
|
|
const viewRef = useRef<HTMLIFrameElement>(null);
|
|
const isReady = useViewIsReady(viewRef);
|
|
const cssFilePath = useThemeCss({ pluginId: props.pluginId, themeId: props.themeId });
|
|
|
|
useEffect(() => {
|
|
if (isReady && props.onReady) props.onReady();
|
|
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
|
}, [isReady]);
|
|
|
|
function frameWindow() {
|
|
if (!viewRef.current) return null;
|
|
return viewRef.current.contentWindow;
|
|
}
|
|
|
|
const postMessage = useCallback((name: string, args: unknown = null) => {
|
|
const win = frameWindow();
|
|
if (!win) return;
|
|
|
|
logger.debug('Got message', name, args);
|
|
|
|
win.postMessage({ target: 'webview', name, args }, '*');
|
|
}, []);
|
|
|
|
const { getFormData } = useFormData(viewRef, postMessage);
|
|
|
|
useImperativeHandle(ref, () => {
|
|
return {
|
|
formData: function() {
|
|
if (viewRef.current) {
|
|
return getFormData();
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
focus: function() {
|
|
if (viewRef.current) focus('UserWebView::focus', viewRef.current);
|
|
},
|
|
};
|
|
}, [getFormData]);
|
|
|
|
const htmlHash = useHtmlLoader(
|
|
viewRef,
|
|
isReady,
|
|
postMessage,
|
|
props.html,
|
|
);
|
|
|
|
const contentSize = useContentSize(
|
|
viewRef,
|
|
htmlHash,
|
|
minWidth,
|
|
minHeight,
|
|
);
|
|
|
|
useSubmitHandler(
|
|
viewRef,
|
|
props.onSubmit,
|
|
props.onDismiss,
|
|
);
|
|
|
|
const windowId = useContext(WindowIdContext);
|
|
useWebviewToPluginMessages(
|
|
viewRef,
|
|
isReady,
|
|
props.pluginId,
|
|
props.viewId,
|
|
windowId,
|
|
postMessage,
|
|
);
|
|
|
|
useScriptLoader(
|
|
postMessage,
|
|
isReady,
|
|
props.scripts,
|
|
cssFilePath,
|
|
);
|
|
|
|
const style = useMemo(() => ({
|
|
'--content-width': `${contentSize.width}px`,
|
|
'--content-height': `${contentSize.height}px`,
|
|
} as React.CSSProperties), [contentSize.width, contentSize.height]);
|
|
|
|
const src = useMemo(() => {
|
|
let isolate = Setting.value('featureFlag.plugins.isolatePluginWebViews');
|
|
isolate ||= needsIsolation(props.pluginId);
|
|
|
|
const path = toForwardSlashes(getAssetPath('services/plugins/UserWebviewIndex.html'));
|
|
if (isolate) {
|
|
return `joplin-content://plugin-webview/${path}`;
|
|
} else {
|
|
return `file://${path}`;
|
|
}
|
|
}, [props.pluginId]);
|
|
|
|
return <iframe
|
|
id={props.viewId}
|
|
style={style}
|
|
className={`plugin-user-webview ${props.fitToContent ? '-fit-to-content' : ''} ${props.borderBottom ? '-border-bottom' : ''}`}
|
|
ref={viewRef}
|
|
src={src}
|
|
></iframe>;
|
|
}
|
|
|
|
export default forwardRef(UserWebview);
|
|
|
|
const needsIsolation = (pluginId: string) => {
|
|
// Some plugins are broken unless isolated from the main application.
|
|
// Always enable isolation for these plugins, even if disabled in settings:
|
|
return ['joplin.plugin.note.tabs', 'joplin.plugin.benji.favorites', 'outline'].includes(pluginId);
|
|
};
|