You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-26 22:41:17 +02:00
Mobile: Rich Text Editor: Enable syntax highlighting and auto-indent in the code block editor (#12909)
This commit is contained in:
@@ -698,7 +698,6 @@ packages/app-mobile/components/NoteEditor/RichTextEditor.js
|
|||||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||||
packages/app-mobile/components/NoteEditor/WarningBanner.js
|
packages/app-mobile/components/NoteEditor/WarningBanner.js
|
||||||
packages/app-mobile/components/NoteEditor/commandDeclarations.js
|
packages/app-mobile/components/NoteEditor/commandDeclarations.js
|
||||||
packages/app-mobile/components/NoteEditor/hooks/useCodeMirrorPlugins.js
|
|
||||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
|
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
|
||||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
|
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
|
||||||
packages/app-mobile/components/NoteEditor/testing/createTestEditorProps.js
|
packages/app-mobile/components/NoteEditor/testing/createTestEditorProps.js
|
||||||
@@ -868,6 +867,7 @@ packages/app-mobile/contentScripts/imageEditorBundle/utils/useEditorMessenger.js
|
|||||||
packages/app-mobile/contentScripts/markdownEditorBundle/contentScript.js
|
packages/app-mobile/contentScripts/markdownEditorBundle/contentScript.js
|
||||||
packages/app-mobile/contentScripts/markdownEditorBundle/types.js
|
packages/app-mobile/contentScripts/markdownEditorBundle/types.js
|
||||||
packages/app-mobile/contentScripts/markdownEditorBundle/useWebViewSetup.js
|
packages/app-mobile/contentScripts/markdownEditorBundle/useWebViewSetup.js
|
||||||
|
packages/app-mobile/contentScripts/markdownEditorBundle/utils/useCodeMirrorPlugins.js
|
||||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.test.js
|
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.test.js
|
||||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.js
|
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.js
|
||||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/index.js
|
packages/app-mobile/contentScripts/rendererBundle/contentScript/index.js
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -671,7 +671,6 @@ packages/app-mobile/components/NoteEditor/RichTextEditor.js
|
|||||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||||
packages/app-mobile/components/NoteEditor/WarningBanner.js
|
packages/app-mobile/components/NoteEditor/WarningBanner.js
|
||||||
packages/app-mobile/components/NoteEditor/commandDeclarations.js
|
packages/app-mobile/components/NoteEditor/commandDeclarations.js
|
||||||
packages/app-mobile/components/NoteEditor/hooks/useCodeMirrorPlugins.js
|
|
||||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
|
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
|
||||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
|
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
|
||||||
packages/app-mobile/components/NoteEditor/testing/createTestEditorProps.js
|
packages/app-mobile/components/NoteEditor/testing/createTestEditorProps.js
|
||||||
@@ -841,6 +840,7 @@ packages/app-mobile/contentScripts/imageEditorBundle/utils/useEditorMessenger.js
|
|||||||
packages/app-mobile/contentScripts/markdownEditorBundle/contentScript.js
|
packages/app-mobile/contentScripts/markdownEditorBundle/contentScript.js
|
||||||
packages/app-mobile/contentScripts/markdownEditorBundle/types.js
|
packages/app-mobile/contentScripts/markdownEditorBundle/types.js
|
||||||
packages/app-mobile/contentScripts/markdownEditorBundle/useWebViewSetup.js
|
packages/app-mobile/contentScripts/markdownEditorBundle/useWebViewSetup.js
|
||||||
|
packages/app-mobile/contentScripts/markdownEditorBundle/utils/useCodeMirrorPlugins.js
|
||||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.test.js
|
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.test.js
|
||||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.js
|
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.js
|
||||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/index.js
|
packages/app-mobile/contentScripts/rendererBundle/contentScript/index.js
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import themeToCss from '@joplin/lib/services/style/themeToCss';
|
|||||||
import ExtendedWebView from '../ExtendedWebView';
|
import ExtendedWebView from '../ExtendedWebView';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useMemo, useCallback } from 'react';
|
import { useMemo, useCallback } from 'react';
|
||||||
import { NativeSyntheticEvent } from 'react-native';
|
import { NativeSyntheticEvent } from 'react-native';
|
||||||
|
|
||||||
import { EditorProps } from './types';
|
import { EditorProps } from './types';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import useCodeMirrorPlugins from './hooks/useCodeMirrorPlugins';
|
|
||||||
import { WebViewErrorEvent } from 'react-native-webview/lib/RNCWebViewNativeComponent';
|
import { WebViewErrorEvent } from 'react-native-webview/lib/RNCWebViewNativeComponent';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import { OnMessageEvent } from '../ExtendedWebView/types';
|
import { OnMessageEvent } from '../ExtendedWebView/types';
|
||||||
@@ -117,16 +115,16 @@ const MarkdownEditor: React.FC<EditorProps> = props => {
|
|||||||
onEditorEvent: props.onEditorEvent,
|
onEditorEvent: props.onEditorEvent,
|
||||||
onAttachFile: props.onAttach,
|
onAttachFile: props.onAttach,
|
||||||
editorOptions: {
|
editorOptions: {
|
||||||
parentElementClassName: 'CodeMirror',
|
parentElementOrClassName: 'CodeMirror',
|
||||||
initialText: props.initialText,
|
initialText: props.initialText,
|
||||||
initialNoteId: props.noteId,
|
initialNoteId: props.noteId,
|
||||||
settings: props.editorSettings,
|
settings: props.editorSettings,
|
||||||
onLocalize: _,
|
|
||||||
},
|
},
|
||||||
webviewRef,
|
webviewRef,
|
||||||
|
pluginStates: props.plugins,
|
||||||
});
|
});
|
||||||
|
|
||||||
props.editorRef.current = editorWebViewSetup.api.editor;
|
props.editorRef.current = editorWebViewSetup.api.mainEditor;
|
||||||
|
|
||||||
const injectedJavaScript = `
|
const injectedJavaScript = `
|
||||||
window.onerror = (message, source, lineno) => {
|
window.onerror = (message, source, lineno) => {
|
||||||
@@ -154,11 +152,6 @@ const MarkdownEditor: React.FC<EditorProps> = props => {
|
|||||||
const css = useCss(props.themeId);
|
const css = useCss(props.themeId);
|
||||||
const html = useHtml();
|
const html = useHtml();
|
||||||
|
|
||||||
const codeMirrorPlugins = useCodeMirrorPlugins(props.plugins);
|
|
||||||
useEffect(() => {
|
|
||||||
void editorWebViewSetup.api.editor.setContentScripts(codeMirrorPlugins);
|
|
||||||
}, [codeMirrorPlugins, editorWebViewSetup]);
|
|
||||||
|
|
||||||
const onMessage = useCallback((event: OnMessageEvent) => {
|
const onMessage = useCallback((event: OnMessageEvent) => {
|
||||||
const data = event.nativeEvent.data;
|
const data = event.nativeEvent.data;
|
||||||
|
|
||||||
@@ -183,7 +176,7 @@ const MarkdownEditor: React.FC<EditorProps> = props => {
|
|||||||
html={html}
|
html={html}
|
||||||
injectedJavaScript={injectedJavaScript}
|
injectedJavaScript={injectedJavaScript}
|
||||||
css={css}
|
css={css}
|
||||||
hasPluginScripts={codeMirrorPlugins.length > 0}
|
hasPluginScripts={editorWebViewSetup.hasPlugins}
|
||||||
onMessage={onMessage}
|
onMessage={onMessage}
|
||||||
onLoadEnd={editorWebViewSetup.webViewEventHandlers.onLoadEnd}
|
onLoadEnd={editorWebViewSetup.webViewEventHandlers.onLoadEnd}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
|
|||||||
@@ -232,6 +232,10 @@ const useEditorControl = (
|
|||||||
onResourceDownloaded: (id: string) => {
|
onResourceDownloaded: (id: string) => {
|
||||||
editorRef.current.onResourceDownloaded(id);
|
editorRef.current.onResourceDownloaded(id);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
remove: () => {
|
||||||
|
editorRef.current.remove();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return control;
|
return control;
|
||||||
@@ -300,6 +304,7 @@ function NoteEditor(props: Props) {
|
|||||||
editorControl.searchControl.hideSearch();
|
editorControl.searchControl.hideSearch();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case EditorEventType.Remove:
|
||||||
case EditorEventType.Scroll:
|
case EditorEventType.Scroll:
|
||||||
// Not handled
|
// Not handled
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -370,6 +370,21 @@ describe('RichTextEditor', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be possible show an editor for math blocks', async () => {
|
||||||
|
let body = 'Test:\n\n$$3^2 + 4^2 = 5^2$$';
|
||||||
|
render(<WrappedEditor
|
||||||
|
noteBody={body}
|
||||||
|
onBodyChange={newBody => { body = newBody; }}
|
||||||
|
/>);
|
||||||
|
|
||||||
|
const editButton = await findElement<HTMLButtonElement>('button.edit');
|
||||||
|
editButton.click();
|
||||||
|
|
||||||
|
const editor = await findElement('dialog .cm-editor');
|
||||||
|
expect(editor).toBeTruthy();
|
||||||
|
expect(editor.textContent).toContain('3^2 + 4^2 = 5^2');
|
||||||
|
});
|
||||||
|
|
||||||
it('should preserve table of contents blocks on edit', async () => {
|
it('should preserve table of contents blocks on edit', async () => {
|
||||||
let body = '# Heading\n\n# Heading 2\n\n[toc]\n\nTest.';
|
let body = '# Heading\n\n# Heading 2\n\n[toc]\n\nTest.';
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,57 @@
|
|||||||
import { createEditor } from '@joplin/editor/CodeMirror';
|
import { createEditor } from '@joplin/editor/CodeMirror';
|
||||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
import WebViewToRNMessenger from '../../utils/ipc/WebViewToRNMessenger';
|
import WebViewToRNMessenger from '../../utils/ipc/WebViewToRNMessenger';
|
||||||
import { EditorProcessApi, EditorProps, MainProcessApi } from './types';
|
import { EditorProcessApi, EditorProps, EditorWithParentProps, ExportedWebViewGlobals, MainProcessApi } from './types';
|
||||||
import readFileToBase64 from '../utils/readFileToBase64';
|
import readFileToBase64 from '../utils/readFileToBase64';
|
||||||
|
import { EditorControl } from '@joplin/editor/types';
|
||||||
|
import { EditorEventType } from '@joplin/editor/events';
|
||||||
|
|
||||||
export { default as setUpLogger } from '../utils/setUpLogger';
|
export { default as setUpLogger } from '../utils/setUpLogger';
|
||||||
|
|
||||||
export const initializeEditor = ({
|
interface ExtendedWindow extends ExportedWebViewGlobals, Window { }
|
||||||
parentElementClassName,
|
declare const window: ExtendedWindow;
|
||||||
|
|
||||||
|
let mainEditor: EditorControl|null = null;
|
||||||
|
let allEditors: EditorControl[] = [];
|
||||||
|
const messenger = new WebViewToRNMessenger<EditorProcessApi, MainProcessApi>('markdownEditor', {
|
||||||
|
get mainEditor() {
|
||||||
|
return mainEditor;
|
||||||
|
},
|
||||||
|
updatePlugins(contentScripts) {
|
||||||
|
for (const editor of allEditors) {
|
||||||
|
void editor.setContentScripts(contentScripts);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateSettings(settings) {
|
||||||
|
for (const editor of allEditors) {
|
||||||
|
editor.updateSettings(settings);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const createEditorWithParent = ({
|
||||||
|
parentElementOrClassName,
|
||||||
initialText,
|
initialText,
|
||||||
initialNoteId,
|
initialNoteId,
|
||||||
settings,
|
settings,
|
||||||
onLocalize,
|
onEvent,
|
||||||
}: EditorProps) => {
|
}: EditorWithParentProps) => {
|
||||||
const messenger = new WebViewToRNMessenger<EditorProcessApi, MainProcessApi>('markdownEditor', null);
|
const parentElement = (() => {
|
||||||
|
if (parentElementOrClassName instanceof HTMLElement) {
|
||||||
const parentElement = document.getElementsByClassName(parentElementClassName)[0] as HTMLElement;
|
return parentElementOrClassName;
|
||||||
|
}
|
||||||
|
return document.getElementsByClassName(parentElementOrClassName)[0] as HTMLElement;
|
||||||
|
})();
|
||||||
if (!parentElement) {
|
if (!parentElement) {
|
||||||
throw new Error(`Unable to find parent element for editor (class name: ${JSON.stringify(parentElementClassName)})`);
|
throw new Error(`Unable to find parent element for editor (class name: ${JSON.stringify(parentElementOrClassName)})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const control = createEditor(parentElement, {
|
const control = createEditor(parentElement, {
|
||||||
initialText,
|
initialText,
|
||||||
initialNoteId,
|
initialNoteId,
|
||||||
settings,
|
settings,
|
||||||
onLocalize,
|
onLocalize: messenger.remoteApi.onLocalize,
|
||||||
|
|
||||||
onPasteFile: async (data) => {
|
onPasteFile: async (data) => {
|
||||||
const base64 = await readFileToBase64(data);
|
const base64 = await readFileToBase64(data);
|
||||||
@@ -34,14 +61,32 @@ export const initializeEditor = ({
|
|||||||
onLogMessage: message => {
|
onLogMessage: message => {
|
||||||
void messenger.remoteApi.logMessage(message);
|
void messenger.remoteApi.logMessage(message);
|
||||||
},
|
},
|
||||||
onEvent: (event): void => {
|
onEvent: (event) => {
|
||||||
void messenger.remoteApi.onEditorEvent(event);
|
onEvent(event);
|
||||||
|
|
||||||
|
if (event.kind === EditorEventType.Remove) {
|
||||||
|
allEditors = allEditors.filter(other => other !== control);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resolveImageSrc: (src) => {
|
resolveImageSrc: (src) => {
|
||||||
return messenger.remoteApi.onResolveImageSrc(src);
|
return messenger.remoteApi.onResolveImageSrc(src);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
allEditors.push(control);
|
||||||
|
void messenger.remoteApi.onEditorAdded();
|
||||||
|
|
||||||
|
return control;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createMainEditor = (props: EditorProps) => {
|
||||||
|
const control = createEditorWithParent({
|
||||||
|
...props,
|
||||||
|
onEvent: (event) => {
|
||||||
|
void messenger.remoteApi.onEditorEvent(event);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Works around https://github.com/laurent22/joplin/issues/10047 by handling
|
// Works around https://github.com/laurent22/joplin/issues/10047 by handling
|
||||||
// the text/uri-list MIME type when pasting, rather than sending the paste event
|
// the text/uri-list MIME type when pasting, rather than sending the paste event
|
||||||
// to CodeMirror.
|
// to CodeMirror.
|
||||||
@@ -57,6 +102,7 @@ export const initializeEditor = ({
|
|||||||
|
|
||||||
// Note: Just adding an onclick listener seems sufficient to focus the editor when its background
|
// Note: Just adding an onclick listener seems sufficient to focus the editor when its background
|
||||||
// is tapped.
|
// is tapped.
|
||||||
|
const parentElement = control.editor.dom.parentElement;
|
||||||
parentElement.addEventListener('click', (event) => {
|
parentElement.addEventListener('click', (event) => {
|
||||||
const activeElement = document.querySelector(':focus');
|
const activeElement = document.querySelector(':focus');
|
||||||
if (!parentElement.contains(activeElement) && event.target === parentElement) {
|
if (!parentElement.contains(activeElement) && event.target === parentElement) {
|
||||||
@@ -64,8 +110,9 @@ export const initializeEditor = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
messenger.setLocalInterface({
|
mainEditor = control;
|
||||||
editor: control,
|
|
||||||
});
|
|
||||||
return control;
|
return control;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.createEditorWithParent = createEditorWithParent;
|
||||||
|
window.createMainEditor = createMainEditor;
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
import { EditorEvent } from '@joplin/editor/events';
|
import { EditorEvent } from '@joplin/editor/events';
|
||||||
import { EditorControl, EditorSettings, OnLocalize } from '@joplin/editor/types';
|
import { ContentScriptData, EditorControl, EditorSettings, LocalizationResult } from '@joplin/editor/types';
|
||||||
|
|
||||||
|
|
||||||
|
export interface EditorProps {
|
||||||
|
parentElementOrClassName: HTMLElement|string;
|
||||||
|
initialText: string;
|
||||||
|
initialNoteId: string|null;
|
||||||
|
settings: EditorSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorWithParentProps extends EditorProps {
|
||||||
|
onEvent: (editorEvent: EditorEvent)=> void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Markdown editor exposes global functions within its <WebView>.
|
||||||
|
// These functions can be used externally.
|
||||||
|
export interface ExportedWebViewGlobals {
|
||||||
|
createEditorWithParent: (options: EditorWithParentProps)=> EditorControl;
|
||||||
|
createMainEditor: (props: EditorProps)=> EditorControl;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EditorProcessApi {
|
export interface EditorProcessApi {
|
||||||
editor: EditorControl;
|
mainEditor: EditorControl;
|
||||||
|
updateSettings: (settings: EditorSettings)=> void;
|
||||||
|
updatePlugins: (contentScripts: ContentScriptData[])=> void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectionRange {
|
export interface SelectionRange {
|
||||||
@@ -10,16 +31,10 @@ export interface SelectionRange {
|
|||||||
end: number;
|
end: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorProps {
|
|
||||||
parentElementClassName: string;
|
|
||||||
initialText: string;
|
|
||||||
initialNoteId: string;
|
|
||||||
onLocalize: OnLocalize;
|
|
||||||
settings: EditorSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MainProcessApi {
|
export interface MainProcessApi {
|
||||||
|
onLocalize(text: string): LocalizationResult;
|
||||||
onEditorEvent(event: EditorEvent): Promise<void>;
|
onEditorEvent(event: EditorEvent): Promise<void>;
|
||||||
|
onEditorAdded(): Promise<void>;
|
||||||
logMessage(message: string): Promise<void>;
|
logMessage(message: string): Promise<void>;
|
||||||
onPasteFile(type: string, dataBase64: string): Promise<void>;
|
onPasteFile(type: string, dataBase64: string): Promise<void>;
|
||||||
onResolveImageSrc(src: string): Promise<string|null>;
|
onResolveImageSrc(src: string): Promise<string|null>;
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { OnMessageEvent, WebViewControl } from '../../components/ExtendedWebView
|
|||||||
import { EditorEvent } from '@joplin/editor/events';
|
import { EditorEvent } from '@joplin/editor/events';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import RNToWebViewMessenger from '../../utils/ipc/RNToWebViewMessenger';
|
import RNToWebViewMessenger from '../../utils/ipc/RNToWebViewMessenger';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||||
|
import useCodeMirrorPlugins from './utils/useCodeMirrorPlugins';
|
||||||
import Resource from '@joplin/lib/models/Resource';
|
import Resource from '@joplin/lib/models/Resource';
|
||||||
import { parseResourceUrl } from '@joplin/lib/urlUtils';
|
import { parseResourceUrl } from '@joplin/lib/urlUtils';
|
||||||
const { isImageMimeType } = require('@joplin/lib/resourceUtils');
|
const { isImageMimeType } = require('@joplin/lib/resourceUtils');
|
||||||
@@ -15,9 +18,10 @@ const logger = Logger.create('markdownEditor');
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
editorOptions: EditorOptions;
|
editorOptions: EditorOptions;
|
||||||
initialSelection: SelectionRange;
|
initialSelection: SelectionRange|null;
|
||||||
noteHash: string;
|
noteHash: string;
|
||||||
globalSearch: string;
|
globalSearch: string;
|
||||||
|
pluginStates: PluginStates;
|
||||||
onEditorEvent: (event: EditorEvent)=> void;
|
onEditorEvent: (event: EditorEvent)=> void;
|
||||||
onAttachFile: (mime: string, base64: string)=> void;
|
onAttachFile: (mime: string, base64: string)=> void;
|
||||||
|
|
||||||
@@ -33,9 +37,11 @@ const defaultSearchState: SearchState = {
|
|||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Result = SetUpResult<EditorProcessApi> & { hasPlugins: boolean };
|
||||||
|
|
||||||
const useWebViewSetup = ({
|
const useWebViewSetup = ({
|
||||||
editorOptions, initialSelection, noteHash, globalSearch, webviewRef, onEditorEvent, onAttachFile,
|
editorOptions, pluginStates, initialSelection, noteHash, globalSearch, webviewRef, onEditorEvent, onAttachFile,
|
||||||
}: Props): SetUpResult<EditorProcessApi> => {
|
}: Props): Result => {
|
||||||
const setInitialSelectionJs = initialSelection ? `
|
const setInitialSelectionJs = initialSelection ? `
|
||||||
cm.select(${initialSelection.start}, ${initialSelection.end});
|
cm.select(${initialSelection.start}, ${initialSelection.end});
|
||||||
cm.execCommand('scrollSelectionIntoView');
|
cm.execCommand('scrollSelectionIntoView');
|
||||||
@@ -51,20 +57,21 @@ const useWebViewSetup = ({
|
|||||||
` : '';
|
` : '';
|
||||||
|
|
||||||
const injectedJavaScript = useMemo(() => `
|
const injectedJavaScript = useMemo(() => `
|
||||||
|
if (typeof markdownEditorBundle === 'undefined') {
|
||||||
|
${shim.injectedJs('markdownEditorBundle')};
|
||||||
|
window.markdownEditorBundle = markdownEditorBundle;
|
||||||
|
markdownEditorBundle.setUpLogger();
|
||||||
|
}
|
||||||
|
|
||||||
if (!window.cm) {
|
if (!window.cm) {
|
||||||
const parentClassName = ${JSON.stringify(editorOptions.parentElementClassName)};
|
const parentClassName = ${JSON.stringify(editorOptions?.parentElementOrClassName)};
|
||||||
const foundParent = document.getElementsByClassName(parentClassName).length > 0;
|
const foundParent = !!parentClassName && document.getElementsByClassName(parentClassName).length > 0;
|
||||||
|
|
||||||
// On Android, injectedJavaScript can be run multiple times, including once before the
|
// On Android, injectedJavaScript can be run multiple times, including once before the
|
||||||
// document has loaded. To avoid logging an error each time the editor starts, don't throw
|
// document has loaded. To avoid logging an error each time the editor starts, don't throw
|
||||||
// if the parent element can't be found:
|
// if the parent element can't be found:
|
||||||
if (foundParent) {
|
if (foundParent) {
|
||||||
${shim.injectedJs('markdownEditorBundle')};
|
window.cm = markdownEditorBundle.createMainEditor(${JSON.stringify(editorOptions)});
|
||||||
markdownEditorBundle.setUpLogger();
|
|
||||||
|
|
||||||
window.cm = markdownEditorBundle.initializeEditor(
|
|
||||||
${JSON.stringify(editorOptions)}
|
|
||||||
);
|
|
||||||
|
|
||||||
${jumpToHashJs}
|
${jumpToHashJs}
|
||||||
// Set the initial selection after jumping to the header -- the initial selection,
|
// Set the initial selection after jumping to the header -- the initial selection,
|
||||||
@@ -75,7 +82,7 @@ const useWebViewSetup = ({
|
|||||||
window.onresize = () => {
|
window.onresize = () => {
|
||||||
cm.execCommand('scrollSelectionIntoView');
|
cm.execCommand('scrollSelectionIntoView');
|
||||||
};
|
};
|
||||||
} else {
|
} else if (parentClassName) {
|
||||||
console.log('No parent element found with class name ', parentClassName);
|
console.log('No parent element found with class name ', parentClassName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,6 +108,10 @@ const useWebViewSetup = ({
|
|||||||
const onAttachRef = useRef(onAttachFile);
|
const onAttachRef = useRef(onAttachFile);
|
||||||
onAttachRef.current = onAttachFile;
|
onAttachRef.current = onAttachFile;
|
||||||
|
|
||||||
|
const codeMirrorPlugins = useCodeMirrorPlugins(pluginStates);
|
||||||
|
const codeMirrorPluginsRef = useRef(codeMirrorPlugins);
|
||||||
|
codeMirrorPluginsRef.current = codeMirrorPlugins;
|
||||||
|
|
||||||
const editorMessenger = useMemo(() => {
|
const editorMessenger = useMemo(() => {
|
||||||
const localApi: MainProcessApi = {
|
const localApi: MainProcessApi = {
|
||||||
async onEditorEvent(event) {
|
async onEditorEvent(event) {
|
||||||
@@ -112,6 +123,13 @@ const useWebViewSetup = ({
|
|||||||
async onPasteFile(type, data) {
|
async onPasteFile(type, data) {
|
||||||
onAttachRef.current(type, data);
|
onAttachRef.current(type, data);
|
||||||
},
|
},
|
||||||
|
async onLocalize(text) {
|
||||||
|
const localizationFunction = _;
|
||||||
|
return localizationFunction(text);
|
||||||
|
},
|
||||||
|
async onEditorAdded() {
|
||||||
|
messenger.remoteApi.updatePlugins(codeMirrorPluginsRef.current);
|
||||||
|
},
|
||||||
async onResolveImageSrc(src) {
|
async onResolveImageSrc(src) {
|
||||||
const url = parseResourceUrl(src);
|
const url = parseResourceUrl(src);
|
||||||
if (!url.itemId) return null;
|
if (!url.itemId) return null;
|
||||||
@@ -153,17 +171,22 @@ const useWebViewSetup = ({
|
|||||||
|
|
||||||
const editorSettings = editorOptions.settings;
|
const editorSettings = editorOptions.settings;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.editor.updateSettings(editorSettings);
|
api.updateSettings(editorSettings);
|
||||||
}, [api, editorSettings]);
|
}, [api, editorSettings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.updatePlugins(codeMirrorPlugins);
|
||||||
|
}, [codeMirrorPlugins, api]);
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
pageSetup: {
|
pageSetup: {
|
||||||
js: injectedJavaScript,
|
js: injectedJavaScript,
|
||||||
css: '',
|
css: '',
|
||||||
},
|
},
|
||||||
|
hasPlugins: codeMirrorPlugins.length > 0,
|
||||||
api,
|
api,
|
||||||
webViewEventHandlers,
|
webViewEventHandlers,
|
||||||
}), [injectedJavaScript, api, webViewEventHandlers]);
|
}), [injectedJavaScript, api, webViewEventHandlers, codeMirrorPlugins]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useWebViewSetup;
|
export default useWebViewSetup;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import '@joplin/editor/ProseMirror/styles';
|
|||||||
import readFileToBase64 from '../../utils/readFileToBase64';
|
import readFileToBase64 from '../../utils/readFileToBase64';
|
||||||
import { EditorLanguageType } from '@joplin/editor/types';
|
import { EditorLanguageType } from '@joplin/editor/types';
|
||||||
import convertHtmlToMarkdown from './convertHtmlToMarkdown';
|
import convertHtmlToMarkdown from './convertHtmlToMarkdown';
|
||||||
|
import { ExportedWebViewGlobals as MarkdownEditorWebViewGlobals } from '../../markdownEditorBundle/types';
|
||||||
|
import { EditorEventType } from '@joplin/editor/events';
|
||||||
|
|
||||||
const postprocessHtml = (html: HTMLElement) => {
|
const postprocessHtml = (html: HTMLElement) => {
|
||||||
// Fix resource URLs
|
// Fix resource URLs
|
||||||
@@ -35,13 +37,16 @@ const htmlToMarkdown = (html: HTMLElement): string => {
|
|||||||
return convertHtmlToMarkdown(html);
|
return convertHtmlToMarkdown(html);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialize = async ({
|
export const initialize = async (
|
||||||
|
{
|
||||||
settings,
|
settings,
|
||||||
initialText,
|
initialText,
|
||||||
initialNoteId,
|
initialNoteId,
|
||||||
parentElementClassName,
|
parentElementClassName,
|
||||||
initialSearch,
|
initialSearch,
|
||||||
}: EditorProps) => {
|
}: EditorProps,
|
||||||
|
markdownEditorApi: MarkdownEditorWebViewGlobals,
|
||||||
|
) => {
|
||||||
const messenger = new WebViewToRNMessenger<EditorProcessApi, MainProcessApi>('rich-text-editor', null);
|
const messenger = new WebViewToRNMessenger<EditorProcessApi, MainProcessApi>('rich-text-editor', null);
|
||||||
const parentElement = document.getElementsByClassName(parentElementClassName)[0];
|
const parentElement = document.getElementsByClassName(parentElementClassName)[0];
|
||||||
if (!parentElement) throw new Error('Parent element not found');
|
if (!parentElement) throw new Error('Parent element not found');
|
||||||
@@ -109,6 +114,18 @@ export const initialize = async ({
|
|||||||
return postprocessHtml(html).outerHTML;
|
return postprocessHtml(html).outerHTML;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
}, (parent, language, onChange) => {
|
||||||
|
return markdownEditorApi.createEditorWithParent({
|
||||||
|
initialText: '',
|
||||||
|
initialNoteId: '',
|
||||||
|
parentElementOrClassName: parent,
|
||||||
|
settings: { ...editor.getSettings(), language },
|
||||||
|
onEvent: (event) => {
|
||||||
|
if (event.kind === EditorEventType.Change) {
|
||||||
|
onChange(event.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
editor.setSearchState(initialSearch);
|
editor.setSearchState(initialSearch);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { SetUpResult } from '../types';
|
|||||||
import { EditorControl, EditorSettings } from '@joplin/editor/types';
|
import { EditorControl, EditorSettings } from '@joplin/editor/types';
|
||||||
import RNToWebViewMessenger from '../../utils/ipc/RNToWebViewMessenger';
|
import RNToWebViewMessenger from '../../utils/ipc/RNToWebViewMessenger';
|
||||||
import { EditorProcessApi, EditorProps, MainProcessApi } from './types';
|
import { EditorProcessApi, EditorProps, MainProcessApi } from './types';
|
||||||
|
import useMarkdownEditorSetup from '../markdownEditorBundle/useWebViewSetup';
|
||||||
import useRendererSetup from '../rendererBundle/useWebViewSetup';
|
import useRendererSetup from '../rendererBundle/useWebViewSetup';
|
||||||
import { EditorEvent } from '@joplin/editor/events';
|
import { EditorEvent } from '@joplin/editor/events';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
@@ -92,7 +93,10 @@ const useMessenger = (props: UseMessengerProps) => {
|
|||||||
}, [props.webviewRef]);
|
}, [props.webviewRef]);
|
||||||
};
|
};
|
||||||
|
|
||||||
type UseSourceProps = Props & { renderer: SetUpResult<RendererControl> };
|
type UseSourceProps = Props & {
|
||||||
|
renderer: SetUpResult<RendererControl>;
|
||||||
|
markdownEditor: SetUpResult<unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
const useSource = (props: UseSourceProps) => {
|
const useSource = (props: UseSourceProps) => {
|
||||||
const propsRef = useRef(props);
|
const propsRef = useRef(props);
|
||||||
@@ -100,6 +104,8 @@ const useSource = (props: UseSourceProps) => {
|
|||||||
|
|
||||||
const rendererJs = props.renderer.pageSetup.js;
|
const rendererJs = props.renderer.pageSetup.js;
|
||||||
const rendererCss = props.renderer.pageSetup.css;
|
const rendererCss = props.renderer.pageSetup.css;
|
||||||
|
const markdownEditorJs = props.markdownEditor.pageSetup.js;
|
||||||
|
const markdownEditorCss = props.markdownEditor.pageSetup.css;
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const editorOptions: EditorProps = {
|
const editorOptions: EditorProps = {
|
||||||
@@ -117,6 +123,7 @@ const useSource = (props: UseSourceProps) => {
|
|||||||
css: `
|
css: `
|
||||||
${shim.injectedCss('richTextEditorBundle')}
|
${shim.injectedCss('richTextEditorBundle')}
|
||||||
${rendererCss}
|
${rendererCss}
|
||||||
|
${markdownEditorCss}
|
||||||
|
|
||||||
/* Increase the size of the editor to make it easier to focus the editor. */
|
/* Increase the size of the editor to make it easier to focus the editor. */
|
||||||
.prosemirror-editor {
|
.prosemirror-editor {
|
||||||
@@ -125,19 +132,23 @@ const useSource = (props: UseSourceProps) => {
|
|||||||
`,
|
`,
|
||||||
js: `
|
js: `
|
||||||
${rendererJs}
|
${rendererJs}
|
||||||
|
${markdownEditorJs}
|
||||||
|
|
||||||
if (!window.richTextEditorCreated) {
|
if (!window.richTextEditorCreated) {
|
||||||
window.richTextEditorCreated = true;
|
window.richTextEditorCreated = true;
|
||||||
${shim.injectedJs('richTextEditorBundle')}
|
${shim.injectedJs('richTextEditorBundle')}
|
||||||
richTextEditorBundle.setUpLogger();
|
richTextEditorBundle.setUpLogger();
|
||||||
richTextEditorBundle.initialize(${JSON.stringify(editorOptions)}).then(function(editor) {
|
richTextEditorBundle.initialize(
|
||||||
|
${JSON.stringify(editorOptions)},
|
||||||
|
window,
|
||||||
|
).then(function(editor) {
|
||||||
/* For testing */
|
/* For testing */
|
||||||
window.joplinRichTextEditor_ = editor;
|
window.joplinRichTextEditor_ = editor;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
}, [rendererJs, rendererCss]);
|
}, [rendererJs, rendererCss, markdownEditorCss, markdownEditorJs]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useWebViewSetup = (props: Props): SetUpResult<EditorControl> => {
|
const useWebViewSetup = (props: Props): SetUpResult<EditorControl> => {
|
||||||
@@ -148,8 +159,23 @@ const useWebViewSetup = (props: Props): SetUpResult<EditorControl> => {
|
|||||||
pluginStates: props.pluginStates,
|
pluginStates: props.pluginStates,
|
||||||
themeId: props.themeId,
|
themeId: props.themeId,
|
||||||
});
|
});
|
||||||
|
const markdownEditor = useMarkdownEditorSetup({
|
||||||
|
webviewRef: props.webviewRef,
|
||||||
|
onAttachFile: props.onAttachFile,
|
||||||
|
initialSelection: null,
|
||||||
|
noteHash: '',
|
||||||
|
globalSearch: props.globalSearch,
|
||||||
|
editorOptions: {
|
||||||
|
settings: props.settings,
|
||||||
|
initialNoteId: null,
|
||||||
|
parentElementOrClassName: '',
|
||||||
|
initialText: '',
|
||||||
|
},
|
||||||
|
onEditorEvent: (_event)=>{},
|
||||||
|
pluginStates: props.pluginStates,
|
||||||
|
});
|
||||||
const messenger = useMessenger({ ...props, renderer });
|
const messenger = useMessenger({ ...props, renderer });
|
||||||
const pageSetup = useSource({ ...props, renderer });
|
const pageSetup = useSource({ ...props, renderer, markdownEditor });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void messenger.remoteApi.editor.updateSettings(props.settings);
|
void messenger.remoteApi.editor.updateSettings(props.settings);
|
||||||
@@ -163,14 +189,16 @@ const useWebViewSetup = (props: Props): SetUpResult<EditorControl> => {
|
|||||||
onLoadEnd: () => {
|
onLoadEnd: () => {
|
||||||
messenger.onWebViewLoaded();
|
messenger.onWebViewLoaded();
|
||||||
renderer.webViewEventHandlers.onLoadEnd();
|
renderer.webViewEventHandlers.onLoadEnd();
|
||||||
|
markdownEditor.webViewEventHandlers.onLoadEnd();
|
||||||
},
|
},
|
||||||
onMessage: (event) => {
|
onMessage: (event) => {
|
||||||
messenger.onWebViewMessage(event);
|
messenger.onWebViewMessage(event);
|
||||||
renderer.webViewEventHandlers.onMessage(event);
|
renderer.webViewEventHandlers.onMessage(event);
|
||||||
|
markdownEditor.webViewEventHandlers.onMessage(event);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [messenger, pageSetup, renderer.webViewEventHandlers]);
|
}, [messenger, pageSetup, renderer.webViewEventHandlers, markdownEditor.webViewEventHandlers]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useWebViewSetup;
|
export default useWebViewSetup;
|
||||||
|
|||||||
@@ -351,6 +351,9 @@ const createEditor = (
|
|||||||
onLogMessage: props.onLogMessage,
|
onLogMessage: props.onLogMessage,
|
||||||
onRemove: () => {
|
onRemove: () => {
|
||||||
editor.destroy();
|
editor.destroy();
|
||||||
|
props.onEvent({
|
||||||
|
kind: EditorEventType.Remove,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
import { ContentScriptData, EditorCommandType, EditorControl, EditorProps, EditorSettings, SearchState, UpdateBodyOptions, UserEventSource } from '../types';
|
import { ContentScriptData, EditorCommandType, EditorControl, EditorProps, EditorSettings, SearchState, UpdateBodyOptions, UserEventSource } from '../types';
|
||||||
import { EditorState, TextSelection, Transaction } from 'prosemirror-state';
|
import { EditorState, TextSelection, Transaction } from 'prosemirror-state';
|
||||||
import { EditorView } from 'prosemirror-view';
|
import { EditorView } from 'prosemirror-view';
|
||||||
@@ -21,17 +22,23 @@ import listPlugin from './plugins/listPlugin';
|
|||||||
import searchExtension from './plugins/searchPlugin';
|
import searchExtension from './plugins/searchPlugin';
|
||||||
import joplinEditorApiPlugin, { setEditorApi } from './plugins/joplinEditorApiPlugin';
|
import joplinEditorApiPlugin, { setEditorApi } from './plugins/joplinEditorApiPlugin';
|
||||||
import linkTooltipPlugin from './plugins/linkTooltipPlugin';
|
import linkTooltipPlugin from './plugins/linkTooltipPlugin';
|
||||||
import { RendererControl } from './types';
|
import { OnCreateCodeEditor as OnCreateCodeEditor, RendererControl } from './types';
|
||||||
import resourcePlaceholderPlugin, { onResourceDownloaded } from './plugins/resourcePlaceholderPlugin';
|
import resourcePlaceholderPlugin, { onResourceDownloaded } from './plugins/resourcePlaceholderPlugin';
|
||||||
import getFileFromPasteEvent from '../utils/getFileFromPasteEvent';
|
import getFileFromPasteEvent from '../utils/getFileFromPasteEvent';
|
||||||
import { RenderResult } from '../../renderer/types';
|
import { RenderResult } from '../../renderer/types';
|
||||||
import detailsPlugin from './plugins/detailsPlugin';
|
import detailsPlugin from './plugins/detailsPlugin';
|
||||||
|
|
||||||
|
interface ProseMirrorControl extends EditorControl {
|
||||||
|
getSettings(): EditorSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const createEditor = async (
|
const createEditor = async (
|
||||||
parentElement: HTMLElement,
|
parentElement: HTMLElement,
|
||||||
props: EditorProps,
|
props: EditorProps,
|
||||||
renderer: RendererControl,
|
renderer: RendererControl,
|
||||||
): Promise<EditorControl> => {
|
createCodeEditor: OnCreateCodeEditor,
|
||||||
|
): Promise<ProseMirrorControl> => {
|
||||||
const renderNodeToMarkup = (node: Node|DocumentFragment) => {
|
const renderNodeToMarkup = (node: Node|DocumentFragment) => {
|
||||||
return renderer.renderHtmlToMarkup(node);
|
return renderer.renderHtmlToMarkup(node);
|
||||||
};
|
};
|
||||||
@@ -91,6 +98,7 @@ const createEditor = async (
|
|||||||
setEditorApi(state.tr, {
|
setEditorApi(state.tr, {
|
||||||
onEvent: props.onEvent,
|
onEvent: props.onEvent,
|
||||||
renderer,
|
renderer,
|
||||||
|
createCodeEditor: createCodeEditor,
|
||||||
localize: async (input: string) => {
|
localize: async (input: string) => {
|
||||||
if (cachedLocalizations.has(input)) {
|
if (cachedLocalizations.has(input)) {
|
||||||
return cachedLocalizations.get(input);
|
return cachedLocalizations.get(input);
|
||||||
@@ -159,7 +167,7 @@ const createEditor = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const editorControl: EditorControl = {
|
const editorControl: ProseMirrorControl = {
|
||||||
supportsCommand: (name: EditorCommandType | string) => {
|
supportsCommand: (name: EditorCommandType | string) => {
|
||||||
return name in commands && !!commands[name as keyof typeof commands];
|
return name in commands && !!commands[name as keyof typeof commands];
|
||||||
},
|
},
|
||||||
@@ -194,6 +202,9 @@ const createEditor = async (
|
|||||||
updateBody: async (newBody: string, _updateBodyOptions?: UpdateBodyOptions) => {
|
updateBody: async (newBody: string, _updateBodyOptions?: UpdateBodyOptions) => {
|
||||||
view.updateState(await createInitialState(newBody));
|
view.updateState(await createInitialState(newBody));
|
||||||
},
|
},
|
||||||
|
getSettings: () => {
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
updateSettings: async (newSettings: EditorSettings) => {
|
updateSettings: async (newSettings: EditorSettings) => {
|
||||||
const oldSettings = settings;
|
const oldSettings = settings;
|
||||||
settings = newSettings;
|
settings = newSettings;
|
||||||
@@ -273,6 +284,11 @@ const createEditor = async (
|
|||||||
const resourceSrc = renderedImage?.src;
|
const resourceSrc = renderedImage?.src;
|
||||||
onResourceDownloaded(view, resourceId, resourceSrc);
|
onResourceDownloaded(view, resourceId, resourceSrc);
|
||||||
},
|
},
|
||||||
|
remove: () => {
|
||||||
|
view.dom.remove();
|
||||||
|
props.onEvent({ kind: EditorEventType.Remove });
|
||||||
|
},
|
||||||
|
focus: () => focus('createEditor', view),
|
||||||
};
|
};
|
||||||
return editorControl;
|
return editorControl;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
import createTextNode from '../../utils/dom/createTextNode';
|
import createTextNode from '../../utils/dom/createTextNode';
|
||||||
import createTextArea from '../../utils/dom/createTextArea';
|
import { EditorApi } from '../joplinEditorApiPlugin';
|
||||||
|
import { EditorLanguageType } from '../../../types';
|
||||||
|
|
||||||
interface SourceBlockData {
|
interface SourceBlockData {
|
||||||
start: string;
|
start: string;
|
||||||
@@ -12,11 +13,12 @@ interface Options {
|
|||||||
editorLabel: string|Promise<string>;
|
editorLabel: string|Promise<string>;
|
||||||
doneLabel: string|Promise<string>;
|
doneLabel: string|Promise<string>;
|
||||||
block: SourceBlockData;
|
block: SourceBlockData;
|
||||||
|
editorApi: EditorApi;
|
||||||
onSave: (newContent: SourceBlockData)=> void;
|
onSave: (newContent: SourceBlockData)=> void;
|
||||||
onDismiss: ()=> void;
|
onDismiss: ()=> void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createEditorDialog = ({ editorLabel, doneLabel, block, onSave, onDismiss }: Options) => {
|
const createEditorDialog = ({ editorApi, doneLabel, block, onSave, onDismiss }: Options) => {
|
||||||
const dialog = document.createElement('dialog');
|
const dialog = document.createElement('dialog');
|
||||||
dialog.classList.add('editor-dialog', '-visible');
|
dialog.classList.add('editor-dialog', '-visible');
|
||||||
document.body.appendChild(dialog);
|
document.body.appendChild(dialog);
|
||||||
@@ -24,21 +26,27 @@ const createEditorDialog = ({ editorLabel, doneLabel, block, onSave, onDismiss }
|
|||||||
dialog.onclose = () => {
|
dialog.onclose = () => {
|
||||||
onDismiss();
|
onDismiss();
|
||||||
dialog.remove();
|
dialog.remove();
|
||||||
|
editor.remove();
|
||||||
};
|
};
|
||||||
|
|
||||||
const { textArea, label: textAreaLabel } = createTextArea({
|
const editor = editorApi.createCodeEditor(
|
||||||
label: editorLabel,
|
dialog,
|
||||||
initialContent: block.content,
|
EditorLanguageType.Markdown,
|
||||||
onChange: (newContent) => {
|
(newContent) => {
|
||||||
block = {
|
block = {
|
||||||
...block,
|
...block,
|
||||||
|
start: '',
|
||||||
|
end: '',
|
||||||
content: newContent,
|
content: newContent,
|
||||||
};
|
};
|
||||||
onSave(block);
|
onSave(block);
|
||||||
},
|
},
|
||||||
spellCheck: false,
|
);
|
||||||
});
|
editor.updateBody([
|
||||||
|
block.start,
|
||||||
|
block.content,
|
||||||
|
block.end,
|
||||||
|
].join(''));
|
||||||
|
|
||||||
const submitButton = document.createElement('button');
|
const submitButton = document.createElement('button');
|
||||||
submitButton.appendChild(createTextNode(doneLabel));
|
submitButton.appendChild(createTextNode(doneLabel));
|
||||||
@@ -47,14 +55,12 @@ const createEditorDialog = ({ editorLabel, doneLabel, block, onSave, onDismiss }
|
|||||||
if (dialog.close) {
|
if (dialog.close) {
|
||||||
dialog.close();
|
dialog.close();
|
||||||
} else {
|
} else {
|
||||||
// .remove the dialog in browsers with limited support for
|
// Handle the case where the dialog element is not supported by the
|
||||||
// HTMLDialogElement (and in JSDOM).
|
// browser/testing environment.
|
||||||
dialog.remove();
|
dialog.onclose(new Event('close'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
dialog.appendChild(textAreaLabel);
|
|
||||||
dialog.appendChild(textArea);
|
|
||||||
dialog.appendChild(submitButton);
|
dialog.appendChild(submitButton);
|
||||||
|
|
||||||
|
|
||||||
@@ -63,7 +69,7 @@ const createEditorDialog = ({ editorLabel, doneLabel, block, onSave, onDismiss }
|
|||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
} else {
|
} else {
|
||||||
dialog.classList.add('-fake-modal');
|
dialog.classList.add('-fake-modal');
|
||||||
focus('createEditorDialog/legacy', textArea);
|
focus('createEditorDialog/legacy', editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ class EditableSourceBlockView implements NodeView {
|
|||||||
createEditorDialog({
|
createEditorDialog({
|
||||||
doneLabel: _('Done'),
|
doneLabel: _('Done'),
|
||||||
editorLabel: _('Code:'),
|
editorLabel: _('Code:'),
|
||||||
|
editorApi: getEditorApi(this.view.state),
|
||||||
block: {
|
block: {
|
||||||
content: this.node.attrs.source,
|
content: this.node.attrs.source,
|
||||||
start: this.node.attrs.openCharacters,
|
start: this.node.attrs.openCharacters,
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { EditorState, Plugin, Transaction } from 'prosemirror-state';
|
import { EditorState, Plugin, Transaction } from 'prosemirror-state';
|
||||||
import { OnEventCallback, OnLocalize } from '../../types';
|
import { OnEventCallback, OnLocalize } from '../../types';
|
||||||
import { RendererControl } from '../types';
|
import { OnCreateCodeEditor, RendererControl } from '../types';
|
||||||
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
|
import createTextArea from '../utils/dom/createTextArea';
|
||||||
|
|
||||||
export interface EditorApi {
|
export interface EditorApi {
|
||||||
renderer: RendererControl;
|
renderer: RendererControl;
|
||||||
onEvent: OnEventCallback;
|
onEvent: OnEventCallback;
|
||||||
|
createCodeEditor: OnCreateCodeEditor;
|
||||||
localize: OnLocalize;
|
localize: OnLocalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,8 +33,23 @@ const joplinEditorApiPlugin = new Plugin<EditorApi>({
|
|||||||
throw new Error('Not initialized');
|
throw new Error('Not initialized');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
settings: null,
|
|
||||||
localize: input => input,
|
localize: input => input,
|
||||||
|
|
||||||
|
// A default implementation for testing environments
|
||||||
|
createCodeEditor: (parent, _language, onChange) => {
|
||||||
|
const editor = createTextArea({ label: 'Editor', initialContent: '', onChange });
|
||||||
|
parent.appendChild(editor.textArea);
|
||||||
|
|
||||||
|
return {
|
||||||
|
focus: () => focus('joplinEditorApiPlugin', editor.textArea),
|
||||||
|
remove: () => {
|
||||||
|
editor.textArea.remove();
|
||||||
|
},
|
||||||
|
updateBody: (newValue) => {
|
||||||
|
editor.textArea.value = newValue;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
apply: (tr, value) => {
|
apply: (tr, value) => {
|
||||||
const proposedValue = tr.getMeta(joplinEditorApiPlugin);
|
const proposedValue = tr.getMeta(joplinEditorApiPlugin);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { RenderResult } from '../../renderer/types';
|
import { RenderResult } from '../../renderer/types';
|
||||||
|
import { EditorLanguageType } from '../types';
|
||||||
|
|
||||||
interface MarkupToHtmlOptions {
|
interface MarkupToHtmlOptions {
|
||||||
isFullPageRender: boolean;
|
isFullPageRender: boolean;
|
||||||
@@ -12,3 +13,15 @@ export interface RendererControl {
|
|||||||
renderMarkupToHtml: MarkupToHtml;
|
renderMarkupToHtml: MarkupToHtml;
|
||||||
renderHtmlToMarkup: HtmlToMarkup;
|
renderHtmlToMarkup: HtmlToMarkup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CodeEditorControl {
|
||||||
|
focus: ()=> void;
|
||||||
|
remove: ()=> void;
|
||||||
|
updateBody: (newValue: string)=> void;
|
||||||
|
}
|
||||||
|
export type OnCodeEditorChange = (newValue: string)=> void;
|
||||||
|
|
||||||
|
// Creates a text editor for editing code blocks
|
||||||
|
export type OnCreateCodeEditor = (
|
||||||
|
parent: HTMLElement, language: EditorLanguageType, onChange: OnCodeEditorChange,
|
||||||
|
)=> CodeEditorControl;
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import createUniqueId from './createUniqueId';
|
|||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
label: LocalizationResult;
|
label: LocalizationResult;
|
||||||
spellCheck: boolean;
|
|
||||||
initialContent: string;
|
initialContent: string;
|
||||||
onChange: (newContent: string)=> void;
|
onChange: (newContent: string)=> void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createTextArea = ({ label, initialContent, spellCheck, onChange }: Options) => {
|
const createTextArea = ({ label, initialContent, onChange }: Options) => {
|
||||||
const textArea = document.createElement('textarea');
|
const textArea = document.createElement('textarea');
|
||||||
textArea.spellcheck = spellCheck;
|
|
||||||
textArea.oninput = () => {
|
textArea.oninput = () => {
|
||||||
onChange(textArea.value);
|
onChange(textArea.value);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export enum EditorEventType {
|
|||||||
EditLink,
|
EditLink,
|
||||||
FollowLink,
|
FollowLink,
|
||||||
Scroll,
|
Scroll,
|
||||||
|
Remove,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChangeEvent {
|
export interface ChangeEvent {
|
||||||
@@ -62,9 +63,13 @@ export interface FollowLinkEvent {
|
|||||||
link: string;
|
link: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RemoveEvent {
|
||||||
|
kind: EditorEventType.Remove;
|
||||||
|
}
|
||||||
|
|
||||||
export type EditorEvent =
|
export type EditorEvent =
|
||||||
ChangeEvent|UndoRedoDepthChangeEvent|SelectionRangeChangeEvent|
|
ChangeEvent|UndoRedoDepthChangeEvent|SelectionRangeChangeEvent|
|
||||||
EditorScrolledEvent|
|
EditorScrolledEvent|
|
||||||
SelectionFormattingChangeEvent|UpdateSearchDialogEvent|
|
SelectionFormattingChangeEvent|UpdateSearchDialogEvent|
|
||||||
RequestEditLinkEvent|FollowLinkEvent;
|
RequestEditLinkEvent|FollowLinkEvent|RemoveEvent;
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,9 @@ export interface EditorControl {
|
|||||||
|
|
||||||
// Called when a resource associated with the current note finishes downloading.
|
// Called when a resource associated with the current note finishes downloading.
|
||||||
onResourceDownloaded(id: string): void;
|
onResourceDownloaded(id: string): void;
|
||||||
|
|
||||||
|
remove(): void;
|
||||||
|
focus(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EditorLanguageType {
|
export enum EditorLanguageType {
|
||||||
|
|||||||
Reference in New Issue
Block a user