Skip to content

Commit

Permalink
Add support for spell checker in CodeMirror using contenteditable mode
Browse files Browse the repository at this point in the history
  • Loading branch information
laurent22 committed Oct 29, 2020
1 parent 8a461a6 commit 305e4c8
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 74 deletions.
181 changes: 109 additions & 72 deletions ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import { _ } from 'lib/locale';
import bridge from '../../../../services/bridge';
import markdownUtils from 'lib/markdownUtils';
import shim from 'lib/shim';
import SpellCheckerService from 'lib/services/spellChecker/SpellCheckerService';

const Note = require('lib/models/Note.js');
const { clipboard } = require('electron');
// const { clipboard } = require('electron');
const shared = require('lib/components/shared/note-screen-shared.js');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
// const MenuItem = bridge().MenuItem;
const { reg } = require('lib/registry.js');
const dialogs = require('../../../dialogs');
const { themeStyle } = require('lib/theme');
Expand Down Expand Up @@ -222,76 +223,78 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
}
}, []);

const editorCutText = useCallback(() => {
if (editorRef.current) {
const selections = editorRef.current.getSelections();
if (selections.length > 0) {
clipboard.writeText(selections[0]);
// Easy way to wipe out just the first selection
selections[0] = '';
editorRef.current.replaceSelections(selections);
}
}
}, []);

const editorCopyText = useCallback(() => {
if (editorRef.current) {
const selections = editorRef.current.getSelections();
if (selections.length > 0) {
clipboard.writeText(selections[0]);
}
}
}, []);

const editorPasteText = useCallback(() => {
if (editorRef.current) {
editorRef.current.replaceSelection(clipboard.readText());
}
}, []);

const onEditorContextMenu = useCallback(() => {
const menu = new Menu();

const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
const clipboardText = clipboard.readText();

menu.append(
new MenuItem({
label: _('Cut'),
enabled: hasSelectedText,
click: async () => {
editorCutText();
},
})
);

menu.append(
new MenuItem({
label: _('Copy'),
enabled: hasSelectedText,
click: async () => {
editorCopyText();
},
})
);

menu.append(
new MenuItem({
label: _('Paste'),
enabled: true,
click: async () => {
if (clipboardText) {
editorPasteText();
} else {
// To handle pasting images
onEditorPaste();
}
},
})
);

menu.popup(bridge().window());
}, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste]);
// const editorCutText = useCallback(() => {
// if (editorRef.current) {
// const selections = editorRef.current.getSelections();
// if (selections.length > 0) {
// clipboard.writeText(selections[0]);
// // Easy way to wipe out just the first selection
// selections[0] = '';
// editorRef.current.replaceSelections(selections);
// }
// }
// }, []);

// const editorCopyText = useCallback(() => {
// if (editorRef.current) {
// const selections = editorRef.current.getSelections();
// if (selections.length > 0) {
// clipboard.writeText(selections[0]);
// }
// }
// }, []);

// const editorPasteText = useCallback(() => {
// if (editorRef.current) {
// editorRef.current.replaceSelection(clipboard.readText());
// }
// }, []);

const onEditorContextMenu = () => {};

// useCallback(() => {
// const menu = new Menu();

// const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
// const clipboardText = clipboard.readText();

// menu.append(
// new MenuItem({
// label: _('Cut'),
// enabled: hasSelectedText,
// click: async () => {
// editorCutText();
// },
// })
// );

// menu.append(
// new MenuItem({
// label: _('Copy'),
// enabled: hasSelectedText,
// click: async () => {
// editorCopyText();
// },
// })
// );

// menu.append(
// new MenuItem({
// label: _('Paste'),
// enabled: true,
// click: async () => {
// if (clipboardText) {
// editorPasteText();
// } else {
// // To handle pasting images
// onEditorPaste();
// }
// },
// })
// );

// menu.popup(bridge().window());
// }, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste]);

const loadScript = async (script:any) => {
return new Promise((resolve) => {
Expand Down Expand Up @@ -604,6 +607,40 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
editorRef.current.refresh();
}, [rootSize, styles.editor, props.visiblePanes]);

// The code below adds support for spell checker when CodeMirror "inputStyle" is "contenteditable"
// however this mode is too buggy to be used in production. Perhaps it can be used if CodeMirror
// contenteditable mode is fixed in a future version.
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
useEffect(() => {
function pointerInsideEditor(x:number, y:number) {
const elements = document.getElementsByClassName('codeMirrorEditor');
if (!elements.length) return null;
console.info(elements[0]);
const rect = elements[0].getBoundingClientRect();
return rect.x < x && rect.y < y && rect.right > x && rect.bottom > y;
}

function onContextMenu(_event:any, params:any) {
if (!pointerInsideEditor(params.x, params.y)) return;

const menu = new Menu();

const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);

for (const item of spellCheckerMenuItems) {
menu.append(item);
}

menu.popup();
}

bridge().window().webContents.on('context-menu', onContextMenu);

return () => {
bridge().window().webContents.off('context-menu', onContextMenu);
};
}, []);

function renderEditor() {

return (
Expand Down
4 changes: 2 additions & 2 deletions ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ function Editor(props: EditorProps, ref: any) {
mode: props.mode,
readOnly: props.readOnly,
autoCloseBrackets: props.autoMatchBraces,
inputStyle: 'textarea', // contenteditable loses cursor position on focus change, use textarea instead
inputStyle: 'contenteditable', // contenteditable loses cursor position on focus change, use textarea instead
lineWrapping: true,
lineNumbers: false,
indentWithTabs: true,
Expand Down Expand Up @@ -236,7 +236,7 @@ function Editor(props: EditorProps, ref: any) {
}
}, [props.keyMap]);

return <div style={props.style} ref={editorParent} />;
return <div className="codeMirrorEditor" contentEditable spellCheck style={props.style} ref={editorParent} />;
}

export default forwardRef(Editor);

0 comments on commit 305e4c8

Please sign in to comment.