import Setting from '@joplin/lib/models/Setting'; import shim from '@joplin/lib/shim'; import { themeStyle } from '@joplin/lib/theme'; const React = require('react'); const { forwardRef, useImperativeHandle, useEffect, useMemo, useState, useCallback, useRef } = require('react'); const { WebView } = require('react-native-webview'); const { editorFont } = require('../global-style'); export interface ChangeEvent { value: string; } export interface UndoRedoDepthChangeEvent { undoDepth: number; redoDepth: number; } export interface Selection { start: number; end: number; } export interface SelectionChangeEvent { selection: Selection; } type ChangeEventHandler = (event: ChangeEvent)=> void; type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void; type SelectionChangeEventHandler = (event: SelectionChangeEvent)=> void; interface Props { themeId: number; initialText: string; initialSelection?: Selection; style: any; onChange: ChangeEventHandler; onSelectionChange: SelectionChangeEventHandler; onUndoRedoDepthChange: UndoRedoDepthChangeHandler; } function fontFamilyFromSettings() { const f = editorFont(Setting.value('style.editor.fontFamily')); return [f, 'sans-serif'].join(', '); } function useCss(themeId: number): string { return useMemo(() => { const theme = themeStyle(themeId); return ` :root { background-color: ${theme.backgroundColor}; } body { font-size: 13pt; } `; }, [themeId]); } function useHtml(css: string): string { const [html, setHtml] = useState(''); useEffect(() => { setHtml( `
` ); }, [css]); return html; } function editorTheme(themeId: number) { return { ...themeStyle(themeId), fontSize: 0.85, // em fontFamily: fontFamilyFromSettings(), }; } function NoteEditor(props: Props, ref: any) { const [source, setSource] = useState(undefined); const webviewRef = useRef(null); const setInitialSelectionJS = props.initialSelection ? ` cm.select(${props.initialSelection.start}, ${props.initialSelection.end}); ` : ''; const injectedJavaScript = ` function postMessage(name, data) { window.ReactNativeWebView.postMessage(JSON.stringify({ data, name, })); } function logMessage(...msg) { postMessage('onLog', { value: msg }); } // This variable is not used within this script // but is called using "injectJavaScript" from // the wrapper component. window.cm = null; try { ${shim.injectedJs('codeMirrorBundle')}; const parentElement = document.getElementsByClassName('CodeMirror')[0]; const theme = ${JSON.stringify(editorTheme(props.themeId))}; const initialText = ${JSON.stringify(props.initialText)}; cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, theme); ${setInitialSelectionJS} // Fixes https://github.com/laurent22/joplin/issues/5949 window.onresize = () => { cm.scrollSelectionIntoView(); }; } catch (e) { window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e)) } true; `; const css = useCss(props.themeId); const html = useHtml(css); useImperativeHandle(ref, () => { return { undo: function() { webviewRef.current.injectJavaScript('cm.undo(); true;'); }, redo: function() { webviewRef.current.injectJavaScript('cm.redo(); true;'); }, select: (anchor: number, head: number) => { webviewRef.current.injectJavaScript( `cm.select(${JSON.stringify(anchor)}, ${JSON.stringify(head)}); true;` ); }, insertText: (text: string) => { webviewRef.current.injectJavaScript(`cm.insertText(${JSON.stringify(text)}); true;`); }, }; }); 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 data = event.nativeEvent.data; if (data.indexOf('error:') === 0) { console.error('CodeMirror:', data); return; } const msg = JSON.parse(data); const handlers: Record