1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Chore: Factor duplicate WebView code into ExtendedWebView.tsx (#6771)

This commit is contained in:
Henry Heino 2022-09-05 04:46:13 -07:00 committed by GitHub
parent b5b281c276
commit 7e1c34b769
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 169 additions and 91 deletions

View File

@ -852,6 +852,9 @@ packages/app-mobile/components/CustomButton.js.map
packages/app-mobile/components/Dropdown.d.ts packages/app-mobile/components/Dropdown.d.ts
packages/app-mobile/components/Dropdown.js packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/Dropdown.js.map packages/app-mobile/components/Dropdown.js.map
packages/app-mobile/components/ExtendedWebView.d.ts
packages/app-mobile/components/ExtendedWebView.js
packages/app-mobile/components/ExtendedWebView.js.map
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.d.ts packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.d.ts
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js.map packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js.map

3
.gitignore vendored
View File

@ -840,6 +840,9 @@ packages/app-mobile/components/CustomButton.js.map
packages/app-mobile/components/Dropdown.d.ts packages/app-mobile/components/Dropdown.d.ts
packages/app-mobile/components/Dropdown.js packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/Dropdown.js.map packages/app-mobile/components/Dropdown.js.map
packages/app-mobile/components/ExtendedWebView.d.ts
packages/app-mobile/components/ExtendedWebView.js
packages/app-mobile/components/ExtendedWebView.js.map
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.d.ts packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.d.ts
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js.map packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js.map

View File

@ -0,0 +1,143 @@
// Wraps react-native-webview. Allows loading HTML directly.
import * as React from 'react';
import {
forwardRef, Ref, useEffect, useImperativeHandle, useRef, useState,
} from 'react';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import { WebViewErrorEvent, WebViewEvent, WebViewSource } from 'react-native-webview/lib/WebViewTypes';
import Setting from '@joplin/lib/models/Setting';
import { themeStyle } from '@joplin/lib/theme';
import { Theme } from '@joplin/lib/themes/type';
import shim from '@joplin/lib/shim';
import { StyleProp, ViewStyle } from 'react-native';
export interface WebViewControl {
// Evaluate the given [script] in the context of the page.
// Unlike react-native-webview/WebView, this does not need to return true.
injectJS(script: string): void;
}
interface SourceFileUpdateEvent {
uri: string;
baseUrl: string;
filePath: string;
}
type OnMessageCallback = (event: WebViewMessageEvent)=> void;
type OnErrorCallback = (event: WebViewErrorEvent)=> void;
type OnLoadEndCallback = (event: WebViewEvent)=> void;
type OnFileUpdateCallback = (event: SourceFileUpdateEvent)=> void;
interface Props {
themeId: number;
// If HTML is still being loaded, [html] should be an empty string.
html: string;
// Allow a secure origin to load content from any other origin.
// Defaults to 'never'.
// See react-native-webview's prop with the same name.
mixedContentMode?: 'never' | 'always';
// Initial javascript. Must evaluate to true.
injectedJavaScript: string;
style?: StyleProp<ViewStyle>;
onMessage: OnMessageCallback;
onError: OnErrorCallback;
onLoadEnd?: OnLoadEndCallback;
// Triggered when the file containing [html] is overwritten with new content.
onFileUpdate?: OnFileUpdateCallback;
}
const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
const theme: Theme = themeStyle(props.themeId);
const webviewRef = useRef(null);
const [source, setSource] = useState<WebViewSource|undefined>(undefined);
useImperativeHandle(ref, (): WebViewControl => {
return {
injectJS(js: string) {
webviewRef.current.injectJavaScript(`
try {
${js}
}
catch(e) {
logMessage('Error in injected JS:' + e, e);
throw e;
};
true;`);
},
};
});
useEffect(() => {
let cancelled = false;
async function createHtmlFile() {
const tempFile = `${Setting.value('resourceDir')}/NoteEditor.html`;
await shim.fsDriver().writeFile(tempFile, props.html, 'utf8');
if (cancelled) return;
// Now that we are sending back a file instead of an HTML string, we're always sending back the
// same file. So we add a cache busting query parameter to it, to make sure that the WebView re-renders.
//
// `baseUrl` is where the images will be loaded from. So images must use a path relative to resourceDir.
const newSource = {
uri: `file://${tempFile}?r=${Math.round(Math.random() * 100000000)}`,
baseUrl: `file://${Setting.value('resourceDir')}/`,
};
setSource(newSource);
props.onFileUpdate?.({
...newSource,
filePath: tempFile,
});
}
if (props.html && props.html.length > 0) {
void createHtmlFile();
} else {
setSource(undefined);
}
return () => {
cancelled = true;
};
}, [props.html, props.onFileUpdate]);
// - `setSupportMultipleWindows` must be `true` for security reasons:
// https://github.com/react-native-webview/react-native-webview/releases/tag/v11.0.0
// - `scrollEnabled` prevents iOS from scrolling the document (has no effect on Android)
// when an editable region (e.g. a the full-screen NoteEditor) is focused.
return (
<WebView
style={{
backgroundColor: theme.backgroundColor,
...(props.style as any),
}}
ref={webviewRef}
scrollEnabled={false}
useWebKit={true}
source={source}
setSupportMultipleWindows={true}
hideKeyboardAccessoryView={true}
allowingReadAccessToURL={`file://${Setting.value('resourceDir')}`}
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
mixedContentMode={props.mixedContentMode}
allowFileAccess={true}
injectedJavaScript={props.injectedJavaScript}
onMessage={props.onMessage}
onError={props.onError}
onLoadEnd={props.onLoadEnd}
/>
);
};
export default forwardRef(ExtendedWebView);

View File

@ -1,16 +1,14 @@
import { useRef, useCallback } from 'react'; import { useRef, useCallback } from 'react';
import Setting from '@joplin/lib/models/Setting';
import useSource from './hooks/useSource'; import useSource from './hooks/useSource';
import useOnMessage from './hooks/useOnMessage'; import useOnMessage from './hooks/useOnMessage';
import useOnResourceLongPress from './hooks/useOnResourceLongPress'; import useOnResourceLongPress from './hooks/useOnResourceLongPress';
const React = require('react'); const React = require('react');
const { View } = require('react-native'); import { View } from 'react-native';
const { WebView } = require('react-native-webview');
const { themeStyle } = require('../global-style.js');
import BackButtonDialogBox from '../BackButtonDialogBox'; import BackButtonDialogBox from '../BackButtonDialogBox';
import { reg } from '@joplin/lib/registry'; import { reg } from '@joplin/lib/registry';
import ExtendedWebView from '../ExtendedWebView';
interface Props { interface Props {
themeId: number; themeId: number;
@ -32,11 +30,9 @@ const webViewStyle = {
}; };
export default function NoteBodyViewer(props: Props) { export default function NoteBodyViewer(props: Props) {
const theme = themeStyle(props.themeId);
const dialogBoxRef = useRef(null); const dialogBoxRef = useRef(null);
const { source, injectedJs } = useSource( const { html, injectedJs } = useSource(
props.noteBody, props.noteBody,
props.noteMarkupLanguage, props.noteMarkupLanguage,
props.themeId, props.themeId,
@ -67,6 +63,8 @@ export default function NoteBodyViewer(props: Props) {
reg.logger().error('WebView error'); reg.logger().error('WebView error');
} }
const BackButtonDialogBox_ = BackButtonDialogBox as any;
// On iOS scalesPageToFit work like this: // On iOS scalesPageToFit work like this:
// //
// Find the widest image, resize it *and everything else* by x% so that // Find the widest image, resize it *and everything else* by x% so that
@ -88,21 +86,14 @@ export default function NoteBodyViewer(props: Props) {
// 2020-10-15: As we've now fully switched to WebKit for iOS (useWebKit=true) and // 2020-10-15: As we've now fully switched to WebKit for iOS (useWebKit=true) and
// since the WebView package went through many versions it's possible that // since the WebView package went through many versions it's possible that
// the above no longer applies. // the above no longer applies.
const BackButtonDialogBox_ = BackButtonDialogBox as any;
return ( return (
<View style={props.style}> <View style={props.style}>
<WebView <ExtendedWebView
theme={theme} themeId={props.themeId}
useWebKit={true}
allowingReadAccessToURL={`file://${Setting.value('resourceDir')}`}
style={webViewStyle} style={webViewStyle}
source={source} html={html}
injectedJavaScript={injectedJs.join('\n')} injectedJavaScript={injectedJs.join('\n')}
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
mixedContentMode="always" mixedContentMode="always"
allowFileAccess={true}
onLoadEnd={onLoadEnd} onLoadEnd={onLoadEnd}
onError={onError} onError={onError}
onMessage={onMessage} onMessage={onMessage}

View File

@ -5,13 +5,9 @@ const { themeStyle } = require('../../global-style.js');
import markupLanguageUtils from '@joplin/lib/markupLanguageUtils'; import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
const { assetsToHeaders } = require('@joplin/renderer'); const { assetsToHeaders } = require('@joplin/renderer');
interface Source {
uri: string;
baseUrl: string;
}
interface UseSourceResult { interface UseSourceResult {
source: Source; // [html] can be null if the note is still being rendered.
html: string|null;
injectedJs: string[]; injectedJs: string[];
} }
@ -24,7 +20,7 @@ function usePrevious(value: any, initialValue: any = null): any {
} }
export default function useSource(noteBody: string, noteMarkupLanguage: number, themeId: number, highlightedKeywords: string[], noteResources: any, paddingBottom: number, noteHash: string): UseSourceResult { export default function useSource(noteBody: string, noteMarkupLanguage: number, themeId: number, highlightedKeywords: string[], noteResources: any, paddingBottom: number, noteHash: string): UseSourceResult {
const [source, setSource] = useState<Source>(undefined); const [html, setHtml] = useState<string>('');
const [injectedJs, setInjectedJs] = useState<string[]>([]); const [injectedJs, setInjectedJs] = useState<string[]>([]);
const [resourceLoadedTime, setResourceLoadedTime] = useState(0); const [resourceLoadedTime, setResourceLoadedTime] = useState(0);
const [isFirstRender, setIsFirstRender] = useState(true); const [isFirstRender, setIsFirstRender] = useState(true);
@ -169,20 +165,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
</html> </html>
`; `;
const tempFile = `${Setting.value('resourceDir')}/NoteBodyViewer.html`; setHtml(html);
await shim.fsDriver().writeFile(tempFile, html, 'utf8');
if (cancelled) return;
// Now that we are sending back a file instead of an HTML string, we're always sending back the
// same file. So we add a cache busting query parameter to it, to make sure that the WebView re-renders.
//
// `baseUrl` is where the images will be loaded from. So images must use a path relative to resourceDir.
setSource({
uri: `file://${tempFile}?r=${Math.round(Math.random() * 100000000)}`,
baseUrl: `file://${Setting.value('resourceDir')}/`,
});
setInjectedJs(js); setInjectedJs(js);
} }
@ -194,7 +177,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
if (isFirstRender) { if (isFirstRender) {
setIsFirstRender(false); setIsFirstRender(false);
setSource(undefined); setHtml('');
setInjectedJs([]); setInjectedJs([]);
} else { } else {
void renderNote(); void renderNote();
@ -206,5 +189,5 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, effectDependencies); }, effectDependencies);
return { source, injectedJs }; return { html, injectedJs };
} }

View File

@ -3,11 +3,11 @@ import shim from '@joplin/lib/shim';
import { themeStyle } from '@joplin/lib/theme'; import { themeStyle } from '@joplin/lib/theme';
import EditLinkDialog from './EditLinkDialog'; import EditLinkDialog from './EditLinkDialog';
import { defaultSearchState, SearchPanel } from './SearchPanel'; import { defaultSearchState, SearchPanel } from './SearchPanel';
import ExtendedWebView from '../ExtendedWebView';
const React = require('react'); const React = require('react');
import { forwardRef, RefObject, useImperativeHandle } from 'react'; import { forwardRef, RefObject, useImperativeHandle } from 'react';
import { useEffect, useMemo, useState, useCallback, useRef } from 'react'; import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
const { WebView } = require('react-native-webview');
import { View, ViewStyle } from 'react-native'; import { View, ViewStyle } from 'react-native';
const { editorFont } = require('../global-style'); const { editorFont } = require('../global-style');
@ -207,7 +207,6 @@ const useEditorControl = (
}; };
function NoteEditor(props: Props, ref: any) { function NoteEditor(props: Props, ref: any) {
const [source, setSource] = useState(undefined);
const webviewRef = useRef(null); const webviewRef = useRef(null);
const setInitialSelectionJS = props.initialSelection ? ` const setInitialSelectionJS = props.initialSelection ? `
@ -284,18 +283,9 @@ function NoteEditor(props: Props, ref: any) {
searchStateRef.current = searchState; searchStateRef.current = searchState;
}, [searchState]); }, [searchState]);
// Runs [js] in the context of the CodeMirror frame. // / Runs [js] in the context of the CodeMirror frame.
const injectJS = (js: string) => { const injectJS = (js: string) => {
webviewRef.current.injectJavaScript(` webviewRef.current.injectJS(js);
try {
${js}
}
catch(e) {
logMessage('Error in injected JS:' + e, e);
throw e;
};
true;`);
}; };
const editorControl = useEditorControl( const editorControl = useEditorControl(
@ -306,26 +296,6 @@ function NoteEditor(props: Props, ref: any) {
return editorControl; return editorControl;
}); });
useEffect(() => {
let cancelled = false;
async function createHtmlFile() {
const tempFile = `${Setting.value('resourceDir')}/NoteEditor.html`;
await shim.fsDriver().writeFile(tempFile, html, 'utf8');
if (cancelled) return;
setSource({
uri: `file://${tempFile}?r=${Math.round(Math.random() * 100000000)}`,
baseUrl: `file://${Setting.value('resourceDir')}/`,
});
}
void createHtmlFile();
return () => {
cancelled = true;
};
}, [html]);
const onMessage = useCallback((event: any) => { const onMessage = useCallback((event: any) => {
const data = event.nativeEvent.data; const data = event.nativeEvent.data;
@ -382,16 +352,10 @@ function NoteEditor(props: Props, ref: any) {
} }
}, [props.onSelectionChange, props.onUndoRedoDepthChange, props.onChange, editorControl]); }, [props.onSelectionChange, props.onUndoRedoDepthChange, props.onChange, editorControl]);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
const onError = useCallback(() => { const onError = useCallback(() => {
console.error('NoteEditor: webview error'); console.error('NoteEditor: webview error');
}, []); }, []);
// - `setSupportMultipleWindows` must be `true` for security reasons:
// https://github.com/react-native-webview/react-native-webview/releases/tag/v11.0.0
// - `scrollEnabled` prevents iOS from scrolling the document (has no effect on Android)
// when the editor is focused.
return ( return (
<View style={{ <View style={{
...props.style, ...props.style,
@ -409,19 +373,10 @@ function NoteEditor(props: Props, ref: any) {
minHeight: '30%', minHeight: '30%',
...props.contentStyle, ...props.contentStyle,
}}> }}>
<WebView <ExtendedWebView
style={{ themeId={props.themeId}
backgroundColor: editorSettings.themeData.backgroundColor,
}}
ref={webviewRef} ref={webviewRef}
scrollEnabled={false} html={html}
useWebKit={true}
source={source}
setSupportMultipleWindows={true}
hideKeyboardAccessoryView={true}
allowingReadAccessToURL={`file://${Setting.value('resourceDir')}`}
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
allowFileAccess={true}
injectedJavaScript={injectedJavaScript} injectedJavaScript={injectedJavaScript}
onMessage={onMessage} onMessage={onMessage}
onError={onError} onError={onError}