文件
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

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);
};