mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Desktop: Add option to choose Code Mirror as code editor (#3284)
This commit is contained in:
parent
a3153f1c9f
commit
a8c8539e7a
@ -69,6 +69,16 @@ ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.js
|
|||||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/index.js
|
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/index.js
|
||||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/types.js
|
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/types.js
|
||||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.js
|
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js
|
||||||
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
||||||
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||||
ElectronClient/gui/NoteEditor/NoteEditor.js
|
ElectronClient/gui/NoteEditor/NoteEditor.js
|
||||||
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -59,6 +59,16 @@ ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.js
|
|||||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/index.js
|
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/index.js
|
||||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/types.js
|
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/types.js
|
||||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.js
|
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
|
||||||
|
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js
|
||||||
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
||||||
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||||
ElectronClient/gui/NoteEditor/NoteEditor.js
|
ElectronClient/gui/NoteEditor/NoteEditor.js
|
||||||
|
@ -1314,10 +1314,12 @@ class Application extends BaseApplication {
|
|||||||
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
|
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
|
||||||
// https://github.com/laurent22/joplin/issues/155
|
// https://github.com/laurent22/joplin/issues/155
|
||||||
|
|
||||||
const css = `.ace_editor * { font-family: ${fontFamilies.join(', ')} !important; }`;
|
const css = `.CodeMirror * { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||||
|
const ace_css = `.ace_editor * { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||||
const styleTag = document.createElement('style');
|
const styleTag = document.createElement('style');
|
||||||
styleTag.type = 'text/css';
|
styleTag.type = 'text/css';
|
||||||
styleTag.appendChild(document.createTextNode(css));
|
styleTag.appendChild(document.createTextNode(css));
|
||||||
|
styleTag.appendChild(document.createTextNode(ace_css));
|
||||||
document.head.appendChild(styleTag);
|
document.head.appendChild(styleTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -866,7 +866,8 @@ class MainScreenComponent extends React.Component {
|
|||||||
const noteContentPropertiesDialogOptions = this.state.noteContentPropertiesDialogOptions;
|
const noteContentPropertiesDialogOptions = this.state.noteContentPropertiesDialogOptions;
|
||||||
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
|
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
|
||||||
|
|
||||||
const bodyEditor = this.props.settingEditorCodeView ? 'AceEditor' : 'TinyMCE';
|
const codeEditor = Setting.value('editor.betaCodeMirror') ? 'CodeMirror' : 'AceEditor';
|
||||||
|
const bodyEditor = this.props.settingEditorCodeView ? codeEditor : 'TinyMCE';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
|
@ -53,7 +53,7 @@ export default function styles(props: NoteBodyEditorProps) {
|
|||||||
fontSize: `${theme.editorFontSize}px`,
|
fontSize: `${theme.editorFontSize}px`,
|
||||||
color: theme.color,
|
color: theme.color,
|
||||||
backgroundColor: theme.backgroundColor,
|
backgroundColor: theme.backgroundColor,
|
||||||
editorTheme: theme.editorTheme, // Defined in theme.js
|
aceEditorTheme: theme.aceEditorTheme, // Defined in theme.js
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
426
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx
Normal file
426
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo } from 'react';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
|
||||||
|
import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
|
||||||
|
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
|
||||||
|
import { useScrollHandler, usePrevious, cursorPositionToTextOffset } from './utils';
|
||||||
|
import Toolbar from './Toolbar';
|
||||||
|
import styles_ from './styles';
|
||||||
|
import { RenderedBody, defaultRenderedBody } from './utils/types';
|
||||||
|
import Editor from './Editor';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
|
// @ts-ignore
|
||||||
|
const Note = require('lib/models/Note.js');
|
||||||
|
const { clipboard } = require('electron');
|
||||||
|
const Setting = require('lib/models/Setting.js');
|
||||||
|
const NoteTextViewer = require('../../../NoteTextViewer.min');
|
||||||
|
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||||
|
const Menu = bridge().Menu;
|
||||||
|
const MenuItem = bridge().MenuItem;
|
||||||
|
const markdownUtils = require('lib/markdownUtils');
|
||||||
|
const { _ } = require('lib/locale');
|
||||||
|
const { reg } = require('lib/registry.js');
|
||||||
|
const dialogs = require('../../../dialogs');
|
||||||
|
|
||||||
|
function markupRenderOptions(override: any = null) {
|
||||||
|
return { ...override };
|
||||||
|
}
|
||||||
|
|
||||||
|
function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||||
|
const styles = styles_(props);
|
||||||
|
|
||||||
|
const [renderedBody, setRenderedBody] = useState<RenderedBody>(defaultRenderedBody()); // Viewer content
|
||||||
|
const [webviewReady, setWebviewReady] = useState(false);
|
||||||
|
|
||||||
|
const previousRenderedBody = usePrevious(renderedBody);
|
||||||
|
const previousSearchMarkers = usePrevious(props.searchMarkers);
|
||||||
|
const previousContentKey = usePrevious(props.contentKey);
|
||||||
|
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
const rootRef = useRef(null);
|
||||||
|
const webviewRef = useRef(null);
|
||||||
|
const props_onChangeRef = useRef<Function>(null);
|
||||||
|
props_onChangeRef.current = props.onChange;
|
||||||
|
const contentKeyHasChangedRef = useRef(false);
|
||||||
|
contentKeyHasChangedRef.current = previousContentKey !== props.contentKey;
|
||||||
|
|
||||||
|
const { resetScroll, editor_scroll, setEditorPercentScroll, setViewerPercentScroll } = useScrollHandler(editorRef, webviewRef, props.onScroll);
|
||||||
|
|
||||||
|
const cancelledKeys: {mac: string[], default: string[]} = { mac: [], default: [] };
|
||||||
|
// Remove Joplin reserved key bindings from the editor
|
||||||
|
const letters = ['F', 'T', 'P', 'Q', 'L', ',', 'G', 'K'];
|
||||||
|
for (let i = 0; i < letters.length; i++) {
|
||||||
|
const l = letters[i];
|
||||||
|
cancelledKeys.default.push(`Ctrl-${l}`);
|
||||||
|
cancelledKeys.mac.push(`Cmd-${l}`);
|
||||||
|
}
|
||||||
|
cancelledKeys.default.push('Alt-E');
|
||||||
|
cancelledKeys.mac.push('Alt-E');
|
||||||
|
|
||||||
|
const codeMirror_change = useCallback((newBody: string) => {
|
||||||
|
props_onChangeRef.current({ changeId: null, content: newBody });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const wrapSelectionWithStrings = useCallback((string1: string, string2 = '', defaultText = '') => {
|
||||||
|
if (!editorRef.current) return;
|
||||||
|
|
||||||
|
if (editorRef.current.somethingSelected()) {
|
||||||
|
editorRef.current.wrapSelections(string1, string2);
|
||||||
|
} else {
|
||||||
|
editorRef.current.wrapSelections(string1 + defaultText, string2);
|
||||||
|
|
||||||
|
// Now select the default text so the user can replace it
|
||||||
|
const selections = editorRef.current.listSelections();
|
||||||
|
const newSelections = [];
|
||||||
|
for (let i = 0; i < selections.length; i++) {
|
||||||
|
const s = selections[i];
|
||||||
|
const anchor = { line: s.anchor.line, ch: s.anchor.ch + string1.length };
|
||||||
|
const head = { line: s.head.line, ch: s.head.ch - string2.length };
|
||||||
|
newSelections.push({ anchor: anchor, head: head });
|
||||||
|
}
|
||||||
|
editorRef.current.setSelections(newSelections);
|
||||||
|
}
|
||||||
|
editorRef.current.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addListItem = useCallback((string1, defaultText = '') => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
if (editorRef.current.somethingSelected()) {
|
||||||
|
editorRef.current.wrapSelectionsByLine(string1);
|
||||||
|
} else if (editorRef.current.getCursor('anchor').ch !== 0) {
|
||||||
|
editorRef.current.insertAtCursor(`\n${string1}`);
|
||||||
|
} else {
|
||||||
|
wrapSelectionWithStrings(string1, '', defaultText);
|
||||||
|
}
|
||||||
|
editorRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [wrapSelectionWithStrings]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => {
|
||||||
|
return {
|
||||||
|
content: () => props.content,
|
||||||
|
resetScroll: () => {
|
||||||
|
resetScroll();
|
||||||
|
},
|
||||||
|
scrollTo: (options:ScrollOptions) => {
|
||||||
|
if (options.type === ScrollOptionTypes.Hash) {
|
||||||
|
if (!webviewRef.current) return;
|
||||||
|
webviewRef.current.wrappedInstance.send('scrollToHash', options.value as string);
|
||||||
|
} else if (options.type === ScrollOptionTypes.Percent) {
|
||||||
|
const p = options.value as number;
|
||||||
|
setEditorPercentScroll(p);
|
||||||
|
setViewerPercentScroll(p);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported scroll options: ${options.type}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
supportsCommand: (/* name:string*/) => {
|
||||||
|
// TODO: not implemented, currently only used for "search" command
|
||||||
|
// which is not directly supported by Ace Editor.
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
execCommand: async (cmd: EditorCommand) => {
|
||||||
|
if (!editorRef.current) return false;
|
||||||
|
|
||||||
|
reg.logger().debug('CodeMirror: execCommand', cmd);
|
||||||
|
|
||||||
|
let commandProcessed = true;
|
||||||
|
|
||||||
|
if (cmd.name === 'dropItems') {
|
||||||
|
if (cmd.value.type === 'notes') {
|
||||||
|
editorRef.current.insertAtCursor(cmd.value.markdownTags.join('\n'));
|
||||||
|
} else if (cmd.value.type === 'files') {
|
||||||
|
const pos = cursorPositionToTextOffset(editorRef.current.getCursor(), props.content);
|
||||||
|
const newBody = await commandAttachFileToBody(props.content, cmd.value.paths, { createFileURL: !!cmd.value.createFileURL, position: pos });
|
||||||
|
editorRef.current.updateBody(newBody);
|
||||||
|
} else {
|
||||||
|
reg.logger().warn('CodeMirror: unsupported drop item: ', cmd);
|
||||||
|
}
|
||||||
|
} else if (cmd.name === 'focus') {
|
||||||
|
editorRef.current.focus();
|
||||||
|
} else {
|
||||||
|
commandProcessed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!commandProcessed) {
|
||||||
|
const commands: any = {
|
||||||
|
textBold: () => wrapSelectionWithStrings('**', '**', _('strong text')),
|
||||||
|
textItalic: () => wrapSelectionWithStrings('*', '*', _('emphasized text')),
|
||||||
|
textLink: async () => {
|
||||||
|
const url = await dialogs.prompt(_('Insert Hyperlink'));
|
||||||
|
if (url) wrapSelectionWithStrings('[', `](${url})`);
|
||||||
|
},
|
||||||
|
textCode: () => {
|
||||||
|
const selections = editorRef.current.getSelections();
|
||||||
|
|
||||||
|
// This bases the selection wrapping only around the first element
|
||||||
|
if (selections.length > 0) {
|
||||||
|
const string = selections[0];
|
||||||
|
|
||||||
|
// Look for newlines
|
||||||
|
const match = string.match(/\r?\n/);
|
||||||
|
|
||||||
|
if (match && match.length > 0) {
|
||||||
|
wrapSelectionWithStrings(`\`\`\`${match[0]}`, `${match[0]}\`\`\``);
|
||||||
|
} else {
|
||||||
|
wrapSelectionWithStrings('`', '`', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
insertText: (value: any) => editorRef.current.insertAtCursor(value),
|
||||||
|
attachFile: async () => {
|
||||||
|
const cursor = editorRef.current.getCursor();
|
||||||
|
const pos = cursorPositionToTextOffset(cursor, props.content);
|
||||||
|
|
||||||
|
const newBody = await commandAttachFileToBody(props.content, null, { position: pos });
|
||||||
|
if (newBody) editorRef.current.updateBody(newBody);
|
||||||
|
},
|
||||||
|
textNumberedList: () => {
|
||||||
|
let bulletNumber = markdownUtils.olLineNumber(editorRef.current.getCurrentLine());
|
||||||
|
if (!bulletNumber) bulletNumber = markdownUtils.olLineNumber(editorRef.current.getPreviousLine());
|
||||||
|
if (!bulletNumber) bulletNumber = 0;
|
||||||
|
addListItem(`${bulletNumber + 1}. `, _('List item'));
|
||||||
|
},
|
||||||
|
textBulletedList: () => addListItem('- ', _('List item')),
|
||||||
|
textCheckbox: () => addListItem('- [ ] ', _('List item')),
|
||||||
|
textHeading: () => addListItem('## ', ''),
|
||||||
|
textHorizontalRule: () => addListItem('* * *'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (commands[cmd.name]) {
|
||||||
|
commands[cmd.name](cmd.value);
|
||||||
|
} else {
|
||||||
|
reg.logger().warn('CodeMirror: unsupported Joplin command: ', cmd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [props.content, addListItem, wrapSelectionWithStrings, setEditorPercentScroll, setViewerPercentScroll, resetScroll, renderedBody]);
|
||||||
|
|
||||||
|
const onEditorPaste = useCallback(async (event: any = null) => {
|
||||||
|
const resourceMds = await handlePasteEvent(event);
|
||||||
|
if (!resourceMds.length) return;
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.replaceSelection(resourceMds.join('\n'));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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 webview_domReady = useCallback(() => {
|
||||||
|
setWebviewReady(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const webview_ipcMessage = useCallback((event: any) => {
|
||||||
|
const msg = event.channel ? event.channel : '';
|
||||||
|
const args = event.args;
|
||||||
|
const arg0 = args && args.length >= 1 ? args[0] : null;
|
||||||
|
|
||||||
|
if (msg.indexOf('checkboxclick:') === 0) {
|
||||||
|
const newBody = shared.toggleCheckbox(msg, props.content);
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.updateBody(newBody);
|
||||||
|
}
|
||||||
|
} else if (msg === 'percentScroll') {
|
||||||
|
setEditorPercentScroll(arg0);
|
||||||
|
} else {
|
||||||
|
props.onMessage(event);
|
||||||
|
}
|
||||||
|
}, [props.onMessage, props.content, setEditorPercentScroll]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const interval = contentKeyHasChangedRef.current ? 0 : 500;
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(async () => {
|
||||||
|
let bodyToRender = props.content;
|
||||||
|
|
||||||
|
if (!bodyToRender.trim() && props.visiblePanes.indexOf('viewer') >= 0 && props.visiblePanes.indexOf('editor') < 0) {
|
||||||
|
// Fixes https://github.com/laurent22/joplin/issues/217
|
||||||
|
bodyToRender = `<i>${_('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout'))}</i>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await props.markupToHtml(props.contentMarkupLanguage, bodyToRender, markupRenderOptions({ resourceInfos: props.resourceInfos }));
|
||||||
|
if (cancelled) return;
|
||||||
|
setRenderedBody(result);
|
||||||
|
}, interval);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [props.content, props.contentMarkupLanguage, props.visiblePanes, props.resourceInfos, props.markupToHtml]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!webviewReady) return;
|
||||||
|
|
||||||
|
const options: any = {
|
||||||
|
pluginAssets: renderedBody.pluginAssets,
|
||||||
|
downloadResources: Setting.value('sync.resourceDownloadMode'),
|
||||||
|
};
|
||||||
|
webviewRef.current.wrappedInstance.send('setHtml', renderedBody.html, options);
|
||||||
|
}, [renderedBody, webviewReady]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.searchMarkers !== previousSearchMarkers || renderedBody !== previousRenderedBody) {
|
||||||
|
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options);
|
||||||
|
}
|
||||||
|
}, [props.searchMarkers, renderedBody]);
|
||||||
|
|
||||||
|
const cellEditorStyle = useMemo(() => {
|
||||||
|
const output = { ...styles.cellEditor };
|
||||||
|
if (!props.visiblePanes.includes('editor')) {
|
||||||
|
output.display = 'none'; // Seems to work fine since the refactoring
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}, [styles.cellEditor, props.visiblePanes]);
|
||||||
|
|
||||||
|
const cellViewerStyle = useMemo(() => {
|
||||||
|
const output = { ...styles.cellViewer };
|
||||||
|
if (!props.visiblePanes.includes('viewer')) {
|
||||||
|
// Note: setting webview.display to "none" is currently not supported due
|
||||||
|
// to this bug: https://github.com/electron/electron/issues/8277
|
||||||
|
// So instead setting the width 0.
|
||||||
|
output.width = 1;
|
||||||
|
output.maxWidth = 1;
|
||||||
|
} else if (!props.visiblePanes.includes('editor')) {
|
||||||
|
output.borderLeftStyle = 'none';
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}, [styles.cellViewer, props.visiblePanes]);
|
||||||
|
|
||||||
|
const editorReadOnly = props.visiblePanes.indexOf('editor') < 0;
|
||||||
|
|
||||||
|
function renderEditor() {
|
||||||
|
return (
|
||||||
|
<div style={cellEditorStyle}>
|
||||||
|
<Editor
|
||||||
|
value={props.content}
|
||||||
|
ref={editorRef}
|
||||||
|
mode={props.contentMarkupLanguage === Note.MARKUP_LANGUAGE_HTML ? 'xml' : 'gfm'}
|
||||||
|
theme={styles.editor.codeMirrorTheme}
|
||||||
|
style={styles.editor}
|
||||||
|
readOnly={props.visiblePanes.indexOf('editor') < 0}
|
||||||
|
autoMatchBraces={Setting.value('editor.autoMatchingBraces')}
|
||||||
|
keyMap={props.keyboardMode}
|
||||||
|
cancelledKeys={cancelledKeys}
|
||||||
|
onChange={codeMirror_change}
|
||||||
|
onScroll={editor_scroll}
|
||||||
|
onEditorContextMenu={onEditorContextMenu}
|
||||||
|
onEditorPaste={onEditorPaste}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderViewer() {
|
||||||
|
return (
|
||||||
|
<div style={cellViewerStyle}>
|
||||||
|
<NoteTextViewer
|
||||||
|
ref={webviewRef}
|
||||||
|
viewerStyle={styles.viewer}
|
||||||
|
onIpcMessage={webview_ipcMessage}
|
||||||
|
onDomReady={webview_domReady}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.root} ref={rootRef}>
|
||||||
|
<div style={styles.rowToolbar}>
|
||||||
|
<Toolbar
|
||||||
|
theme={props.theme}
|
||||||
|
dispatch={props.dispatch}
|
||||||
|
disabled={editorReadOnly}
|
||||||
|
/>
|
||||||
|
{props.noteToolbar}
|
||||||
|
</div>
|
||||||
|
<div style={styles.rowEditorViewer}>
|
||||||
|
{renderEditor()}
|
||||||
|
{renderViewer()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef(CodeMirror);
|
||||||
|
|
182
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx
Normal file
182
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useEffect, useImperativeHandle, useState, useRef, useCallback, forwardRef } from 'react';
|
||||||
|
|
||||||
|
const CodeMirror = require('codemirror');
|
||||||
|
import 'codemirror/addon/comment/comment';
|
||||||
|
import 'codemirror/addon/dialog/dialog';
|
||||||
|
import 'codemirror/addon/edit/closebrackets';
|
||||||
|
import 'codemirror/addon/edit/continuelist';
|
||||||
|
import 'codemirror/addon/scroll/scrollpastend';
|
||||||
|
|
||||||
|
import useListIdent from './utils/useListIdent';
|
||||||
|
import useScrollUtils from './utils/useScrollUtils';
|
||||||
|
import useCursorUtils from './utils/useCursorUtils';
|
||||||
|
import useLineSorting from './utils/useLineSorting';
|
||||||
|
|
||||||
|
import 'codemirror/keymap/emacs';
|
||||||
|
import 'codemirror/keymap/vim';
|
||||||
|
|
||||||
|
import 'codemirror/mode/gfm/gfm';
|
||||||
|
import 'codemirror/mode/xml/xml';
|
||||||
|
// Modes for syntax highlighting inside of code blocks
|
||||||
|
import 'codemirror/mode/python/python';
|
||||||
|
import 'codemirror/mode/javascript/javascript';
|
||||||
|
import 'codemirror/mode/markdown/markdown';
|
||||||
|
import 'codemirror/mode/clike/clike';
|
||||||
|
import 'codemirror/mode/diff/diff';
|
||||||
|
import 'codemirror/mode/sql/sql';
|
||||||
|
|
||||||
|
export interface CancelledKeys {
|
||||||
|
mac: string[],
|
||||||
|
default: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorProps {
|
||||||
|
value: string,
|
||||||
|
mode: string,
|
||||||
|
style: any,
|
||||||
|
theme: any,
|
||||||
|
readOnly: boolean,
|
||||||
|
autoMatchBraces: boolean,
|
||||||
|
keyMap: string,
|
||||||
|
cancelledKeys: CancelledKeys,
|
||||||
|
onChange: any,
|
||||||
|
onScroll: any,
|
||||||
|
onEditorContextMenu: any,
|
||||||
|
onEditorPaste: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
function Editor(props: EditorProps, ref: any) {
|
||||||
|
const [editor, setEditor] = useState(null);
|
||||||
|
const editorParent = useRef(null);
|
||||||
|
|
||||||
|
// Codemirror plugins add new commands to codemirror (or change it's behavior)
|
||||||
|
// This command adds the smartListIndent function which will be bound to tab
|
||||||
|
useListIdent(CodeMirror);
|
||||||
|
useScrollUtils(CodeMirror);
|
||||||
|
useCursorUtils(CodeMirror);
|
||||||
|
useLineSorting(CodeMirror);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.cancelledKeys) {
|
||||||
|
for (let i = 0; i < props.cancelledKeys.mac.length; i++) {
|
||||||
|
const k = props.cancelledKeys.mac[i];
|
||||||
|
CodeMirror.keyMap.macDefault[k] = null;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < props.cancelledKeys.default.length; i++) {
|
||||||
|
const k = props.cancelledKeys.default[i];
|
||||||
|
CodeMirror.keyMap.default[k] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [props.cancelledKeys]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => {
|
||||||
|
return editor;
|
||||||
|
});
|
||||||
|
|
||||||
|
const editor_change = useCallback((cm: any, change: any) => {
|
||||||
|
if (props.onChange && change.origin !== 'setValue') {
|
||||||
|
props.onChange(cm.getValue());
|
||||||
|
}
|
||||||
|
}, [props.onChange]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
|
const editor_scroll = useCallback((_cm: any) => {
|
||||||
|
props.onScroll();
|
||||||
|
}, [props.onScroll]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
|
const editor_mousedown = useCallback((_cm: any, event: any) => {
|
||||||
|
if (event && event.button === 2) {
|
||||||
|
props.onEditorContextMenu();
|
||||||
|
}
|
||||||
|
}, [props.onEditorContextMenu]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
|
const editor_paste = useCallback((_cm: any, _event: any) => {
|
||||||
|
props.onEditorPaste();
|
||||||
|
}, [props.onEditorPaste]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
|
const editor_drop = useCallback((cm: any, _event: any) => {
|
||||||
|
cm.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const editor_drag = useCallback((cm: any, event: any) => {
|
||||||
|
// This is the type for all drag and drops that are external to codemirror
|
||||||
|
// setting the cursor allows us to drop them in the right place
|
||||||
|
if (event.dataTransfer.effectAllowed === 'all') {
|
||||||
|
const coords = cm.coordsChar({ left: event.x, top: event.y });
|
||||||
|
cm.setCursor(coords);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// const divRef = useCallback(node => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editorParent.current) return () => {};
|
||||||
|
|
||||||
|
const cmOptions = {
|
||||||
|
value: props.value,
|
||||||
|
screenReaderLabel: props.value,
|
||||||
|
theme: props.theme,
|
||||||
|
mode: props.mode,
|
||||||
|
readOnly: props.readOnly,
|
||||||
|
autoCloseBrackets: props.autoMatchBraces,
|
||||||
|
inputStyle: 'textarea', // contenteditable loses cursor position on focus change, use textarea instead
|
||||||
|
lineWrapping: true,
|
||||||
|
lineNumbers: false,
|
||||||
|
scrollPastEnd: true,
|
||||||
|
indentWithTabs: true,
|
||||||
|
indentUnit: 4,
|
||||||
|
spellcheck: true,
|
||||||
|
allowDropFileTypes: [''], // disable codemirror drop handling
|
||||||
|
keyMap: props.keyMap ? props.keyMap : 'default',
|
||||||
|
extraKeys: { 'Enter': 'insertListElement',
|
||||||
|
'Ctrl-/': 'toggleComment',
|
||||||
|
'Ctrl-Alt-S': 'sortSelectedLines',
|
||||||
|
'Tab': 'smartListIndent',
|
||||||
|
'Shift-Tab': 'smartListUnindent' },
|
||||||
|
};
|
||||||
|
const cm = CodeMirror(editorParent.current, cmOptions);
|
||||||
|
setEditor(cm);
|
||||||
|
cm.on('change', editor_change);
|
||||||
|
cm.on('scroll', editor_scroll);
|
||||||
|
cm.on('mousedown', editor_mousedown);
|
||||||
|
cm.on('paste', editor_paste);
|
||||||
|
cm.on('drop', editor_drop);
|
||||||
|
cm.on('dragover', editor_drag);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Clean up codemirror
|
||||||
|
cm.off('change', editor_change);
|
||||||
|
cm.off('scroll', editor_scroll);
|
||||||
|
cm.off('mousedown', editor_mousedown);
|
||||||
|
cm.off('paste', editor_paste);
|
||||||
|
cm.off('drop', editor_drop);
|
||||||
|
cm.off('dragover', editor_drag);
|
||||||
|
editorParent.current.removeChild(cm.getWrapperElement());
|
||||||
|
setEditor(null);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor) {
|
||||||
|
// Value can also be changed by the editor itself so we need this guard
|
||||||
|
// to prevent loops
|
||||||
|
if (props.value !== editor.getValue()) {
|
||||||
|
editor.setValue(props.value);
|
||||||
|
editor.clearHistory();
|
||||||
|
}
|
||||||
|
editor.setOption('screenReaderLabel', props.value);
|
||||||
|
editor.setOption('theme', props.theme);
|
||||||
|
editor.setOption('mode', props.mode);
|
||||||
|
editor.setOption('readOnly', props.readOnly);
|
||||||
|
editor.setOption('autoCloseBrackets', props.autoMatchBraces);
|
||||||
|
editor.setOption('keyMap', props.keyMap ? props.keyMap : 'default');
|
||||||
|
}
|
||||||
|
}, [props.value, props.theme, props.mode, props.readOnly, props.autoMatchBraces, props.keyMap]);
|
||||||
|
|
||||||
|
return <div style={props.style} ref={editorParent} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef(Editor);
|
169
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx
Normal file
169
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const ToolbarBase = require('../../../Toolbar.min.js');
|
||||||
|
const { _ } = require('lib/locale');
|
||||||
|
const { buildStyle, themeStyle } = require('../../../../theme.js');
|
||||||
|
|
||||||
|
interface ToolbarProps {
|
||||||
|
theme: number,
|
||||||
|
dispatch: Function,
|
||||||
|
disabled: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
function styles_(props:ToolbarProps) {
|
||||||
|
return buildStyle('AceEditorToolbar', props.theme, (/* theme:any*/) => {
|
||||||
|
const theme = themeStyle(props.theme);
|
||||||
|
return {
|
||||||
|
root: {
|
||||||
|
flex: 1,
|
||||||
|
marginBottom: 0,
|
||||||
|
borderTop: `1px solid ${theme.dividerColor}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Toolbar(props:ToolbarProps) {
|
||||||
|
const styles = styles_(props);
|
||||||
|
|
||||||
|
function createToolbarItems() {
|
||||||
|
const toolbarItems = [];
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
tooltip: _('Bold'),
|
||||||
|
iconName: 'fa-bold',
|
||||||
|
onClick: () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'textBold',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
tooltip: _('Italic'),
|
||||||
|
iconName: 'fa-italic',
|
||||||
|
onClick: () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'textItalic',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
type: 'separator',
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
tooltip: _('Hyperlink'),
|
||||||
|
iconName: 'fa-link',
|
||||||
|
onClick: () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'textLink',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
tooltip: _('Code'),
|
||||||
|
iconName: 'fa-code',
|
||||||
|
onClick: () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'textCode',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
tooltip: _('Attach file'),
|
||||||
|
iconName: 'fa-paperclip',
|
||||||
|
onClick: () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'attachFile',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
type: 'separator',
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
tooltip: _('Numbered List'),
|
||||||
|
iconName: 'fa-list-ol',
|
||||||
|
onClick: () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'textNumberedList',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
tooltip: _('Bulleted List'),
|
||||||
|
iconName: 'fa-list-ul',
|
||||||
|
onClick: () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'textBulletedList',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
tooltip: _('Checkbox'),
|
||||||
|
iconName: 'fa-check-square',
|
||||||
|
onClick: () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'textCheckbox',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
tooltip: _('Heading'),
|
||||||
|
iconName: 'fa-heading',
|
||||||
|
onClick: () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'textHeading',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
tooltip: _('Horizontal Rule'),
|
||||||
|
iconName: 'fa-ellipsis-h',
|
||||||
|
onClick: () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'textHorizontalRule',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
tooltip: _('Insert Date Time'),
|
||||||
|
iconName: 'fa-calendar-plus',
|
||||||
|
onClick: () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'insertDateTime',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarItems.push({
|
||||||
|
type: 'separator',
|
||||||
|
});
|
||||||
|
|
||||||
|
return toolbarItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ToolbarBase disabled={props.disabled} style={styles.root} items={createToolbarItems()} />;
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
import { NoteBodyEditorProps } from '../../../utils/types';
|
||||||
|
const { buildStyle } = require('../../../../../theme.js');
|
||||||
|
|
||||||
|
export default function styles(props: NoteBodyEditorProps) {
|
||||||
|
return buildStyle('AceEditor', props.theme, (theme: any) => {
|
||||||
|
return {
|
||||||
|
root: {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
...props.style,
|
||||||
|
},
|
||||||
|
rowToolbar: {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
rowEditorViewer: {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: 10,
|
||||||
|
},
|
||||||
|
cellEditor: {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
cellViewer: {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
borderLeftColor: theme.dividerColor,
|
||||||
|
borderLeftStyle: 'solid',
|
||||||
|
},
|
||||||
|
viewer: {
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
editor: {
|
||||||
|
display: 'flex',
|
||||||
|
width: 'auto',
|
||||||
|
height: 'auto',
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'hidden',
|
||||||
|
paddingTop: 0,
|
||||||
|
lineHeight: `${theme.textAreaLineHeight}px`,
|
||||||
|
fontSize: `${theme.editorFontSize}px`,
|
||||||
|
color: theme.color,
|
||||||
|
backgroundColor: theme.backgroundColor,
|
||||||
|
codeMirrorTheme: theme.codeMirrorTheme, // Defined in theme.js
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
import { useEffect, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
export function cursorPositionToTextOffset(cursorPos: any, body: string) {
|
||||||
|
if (!body) return 0;
|
||||||
|
|
||||||
|
const noteLines = body.split('\n');
|
||||||
|
|
||||||
|
let pos = 0;
|
||||||
|
for (let i = 0; i < noteLines.length; i++) {
|
||||||
|
if (i > 0) pos++; // Need to add the newline that's been removed in the split() call above
|
||||||
|
|
||||||
|
if (i === cursorPos.line) {
|
||||||
|
pos += cursorPos.ch;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
pos += noteLines[i].length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePrevious(value: any): any {
|
||||||
|
const ref = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
});
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useScrollHandler(editorRef: any, webviewRef: any, onScroll: Function) {
|
||||||
|
const ignoreNextEditorScrollEvent_ = useRef(false);
|
||||||
|
const scrollTimeoutId_ = useRef<any>(null);
|
||||||
|
|
||||||
|
const scheduleOnScroll = useCallback((event: any) => {
|
||||||
|
if (scrollTimeoutId_.current) {
|
||||||
|
clearTimeout(scrollTimeoutId_.current);
|
||||||
|
scrollTimeoutId_.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTimeoutId_.current = setTimeout(() => {
|
||||||
|
scrollTimeoutId_.current = null;
|
||||||
|
onScroll(event);
|
||||||
|
}, 10);
|
||||||
|
}, [onScroll]);
|
||||||
|
|
||||||
|
const setEditorPercentScroll = useCallback((p: number) => {
|
||||||
|
ignoreNextEditorScrollEvent_.current = true;
|
||||||
|
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.setScrollPercent(p);
|
||||||
|
|
||||||
|
scheduleOnScroll({ percent: p });
|
||||||
|
}
|
||||||
|
}, [scheduleOnScroll]);
|
||||||
|
|
||||||
|
const setViewerPercentScroll = useCallback((p: number) => {
|
||||||
|
if (webviewRef.current) {
|
||||||
|
webviewRef.current.wrappedInstance.send('setPercentScroll', p);
|
||||||
|
scheduleOnScroll({ percent: p });
|
||||||
|
}
|
||||||
|
}, [scheduleOnScroll]);
|
||||||
|
|
||||||
|
const editor_scroll = useCallback(() => {
|
||||||
|
if (ignoreNextEditorScrollEvent_.current) {
|
||||||
|
ignoreNextEditorScrollEvent_.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editorRef.current) {
|
||||||
|
const percent = editorRef.current.getScrollPercent();
|
||||||
|
|
||||||
|
setViewerPercentScroll(percent);
|
||||||
|
}
|
||||||
|
}, [setViewerPercentScroll]);
|
||||||
|
|
||||||
|
const resetScroll = useCallback(() => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.setScrollPercent(0);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll };
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
export interface RenderedBody {
|
||||||
|
html: string;
|
||||||
|
pluginAssets: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultRenderedBody(): RenderedBody {
|
||||||
|
return {
|
||||||
|
html: '',
|
||||||
|
pluginAssets: [],
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
// Helper functions that use the cursor
|
||||||
|
export default function useCursorUtils(CodeMirror: any) {
|
||||||
|
|
||||||
|
CodeMirror.defineExtension('insertAtCursor', function(text: string) {
|
||||||
|
// This is also the method to get all cursors
|
||||||
|
const ranges = this.listSelections();
|
||||||
|
// Batches the insert operations, if this wasn't done the inserts
|
||||||
|
// could potentially overwrite one another
|
||||||
|
this.operation(() => {
|
||||||
|
for (let i = 0; i < ranges.length; i++) {
|
||||||
|
// anchor is where the selection starts, and head is where it ends
|
||||||
|
// this changes based on how the uses makes a selection
|
||||||
|
const { anchor, head } = ranges[i];
|
||||||
|
// We want the selection that comes first in the document
|
||||||
|
let range = anchor;
|
||||||
|
if (head.line < anchor.line || (head.line === anchor.line && head.ch < anchor.ch)) {
|
||||||
|
range = head;
|
||||||
|
}
|
||||||
|
this.replaceRange(text, range);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineExtension('getCurrentLine', function() {
|
||||||
|
const curs = this.getCursor('anchor');
|
||||||
|
|
||||||
|
return this.getLine(curs.line);
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineExtension('getPreviousLine', function() {
|
||||||
|
const curs = this.getCursor('anchor');
|
||||||
|
|
||||||
|
if (curs.line > 0) { return this.getLine(curs.line - 1); }
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// this updates the body in a way that registers with the undo/redo
|
||||||
|
CodeMirror.defineExtension('updateBody', function(newBody: string) {
|
||||||
|
const start = { line: this.firstLine(), ch: 0 };
|
||||||
|
const last = this.getLine(this.lastLine());
|
||||||
|
const end = { line: this.lastLine(), ch: last ? last.length : 0 };
|
||||||
|
|
||||||
|
this.replaceRange(newBody, start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineExtension('wrapSelections', function(string1: string, string2: string) {
|
||||||
|
const selectedStrings = this.getSelections();
|
||||||
|
|
||||||
|
// Batches the insert operations, if this wasn't done the inserts
|
||||||
|
// could potentially overwrite one another
|
||||||
|
this.operation(() => {
|
||||||
|
for (let i = 0; i < selectedStrings.length; i++) {
|
||||||
|
const selected = selectedStrings[i];
|
||||||
|
|
||||||
|
// Remove white space on either side of selection
|
||||||
|
const start = selected.search(/[^\s]/);
|
||||||
|
const end = selected.search(/[^\s](?=[\s]*$)/);
|
||||||
|
const core = selected.substr(start, end - start + 1);
|
||||||
|
|
||||||
|
// If selection can be toggled do that
|
||||||
|
if (core.startsWith(string1) && core.endsWith(string2)) {
|
||||||
|
const inside = core.substr(string1.length, core.length - string1.length - string2.length);
|
||||||
|
selectedStrings[i] = selected.substr(0, start) + inside + selected.substr(end + 1);
|
||||||
|
} else {
|
||||||
|
selectedStrings[i] = selected.substr(0, start) + string1 + core + string2 + selected.substr(end + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.replaceSelections(selectedStrings, 'around');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineExtension('wrapSelectionsByLine', function(string1: string) {
|
||||||
|
const selectedStrings = this.getSelections();
|
||||||
|
|
||||||
|
// Batches the insert operations, if this wasn't done the inserts
|
||||||
|
// could potentially overwrite one another
|
||||||
|
this.operation(() => {
|
||||||
|
for (let i = 0; i < selectedStrings.length; i++) {
|
||||||
|
const selected = selectedStrings[i];
|
||||||
|
|
||||||
|
const lines = selected.split(/\r?\n/);
|
||||||
|
// Save the newline character to restore it later
|
||||||
|
const newLines = selected.match(/\r?\n/);
|
||||||
|
|
||||||
|
for (let j = 0; j < lines.length; j++) {
|
||||||
|
const line = lines[j];
|
||||||
|
// Only add the list token if it's not already there
|
||||||
|
// if it is, remove it
|
||||||
|
if (!line.startsWith(string1)) {
|
||||||
|
lines[j] = string1 + line;
|
||||||
|
} else {
|
||||||
|
lines[j] = line.substr(string1.length, line.length - string1.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLine = newLines !== null ? newLines[0] : '\n';
|
||||||
|
selectedStrings[i] = lines.join(newLine);
|
||||||
|
}
|
||||||
|
this.replaceSelections(selectedStrings, 'around');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
// Duplicates AceEditors line sorting function
|
||||||
|
// https://discourse.joplinapp.org/t/sort-lines/8874/2
|
||||||
|
export default function useLineSorting(CodeMirror: any) {
|
||||||
|
CodeMirror.commands.sortSelectedLines = function(cm: any) {
|
||||||
|
const ranges = cm.listSelections();
|
||||||
|
// Batches the insert operations, if this wasn't done the inserts
|
||||||
|
// could potentially overwrite one another
|
||||||
|
cm.operation(() => {
|
||||||
|
for (let i = 0; i < ranges.length; i++) {
|
||||||
|
// anchor is where the selection starts, and head is where it ends
|
||||||
|
// this changes based on how the uses makes a selection
|
||||||
|
const { anchor, head } = ranges[i];
|
||||||
|
const start = Math.min(anchor.line, head.line);
|
||||||
|
const end = Math.max(anchor.line, head.line);
|
||||||
|
|
||||||
|
const lines = [];
|
||||||
|
for (let j = start; j <= end; j++) {
|
||||||
|
lines.push(cm.getLine(j));
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = lines.sort().join('\n');
|
||||||
|
// Get the end of the last line
|
||||||
|
const ch = lines[lines.length - 1].length;
|
||||||
|
|
||||||
|
cm.replaceRange(text, { line: start, ch: 0 }, { line: end, ch: ch });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
const { isListItem, isEmptyListItem, extractListToken, olLineNumber } = require('lib/markdownUtils');
|
||||||
|
|
||||||
|
// Markdown list indentation.
|
||||||
|
// If the current line starts with `markup.list` token,
|
||||||
|
// hitting `Tab` key indents the line instead of inserting tab at cursor.
|
||||||
|
// hitting enter will insert a new list element, and unindent/delete an empty element
|
||||||
|
export default function useListIdent(CodeMirror: any) {
|
||||||
|
|
||||||
|
function isSelection(anchor: any, head: any) {
|
||||||
|
return anchor.line !== head.line || anchor.ch !== head.ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndentLevel(cm: any, line: number) {
|
||||||
|
const tokens = cm.getLineTokens(line);
|
||||||
|
let indentLevel = 0;
|
||||||
|
if (tokens.length > 0 && tokens[0].string.match(/\s/)) {
|
||||||
|
indentLevel = tokens[0].string.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return indentLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function newListToken(cm: any, line: number) {
|
||||||
|
const currentToken = extractListToken(cm.getLine(line));
|
||||||
|
const indentLevel = getIndentLevel(cm, line);
|
||||||
|
|
||||||
|
while (--line > 0) {
|
||||||
|
const currentLine = cm.getLine(line);
|
||||||
|
if (!isListItem(currentLine)) return currentToken;
|
||||||
|
|
||||||
|
const indent = getIndentLevel(cm, line);
|
||||||
|
|
||||||
|
if (indent < indentLevel - 1) return currentToken;
|
||||||
|
|
||||||
|
if (indent === indentLevel - 1) {
|
||||||
|
if (olLineNumber(currentLine)) {
|
||||||
|
return `${olLineNumber(currentLine) + 1}. `;
|
||||||
|
}
|
||||||
|
const token = extractListToken(currentLine);
|
||||||
|
if (token.match(/x/)) {
|
||||||
|
return '- [ ] ';
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the first non-whitespace token locationof a list
|
||||||
|
function getListSpan(listTokens: any) {
|
||||||
|
let start = listTokens[0].start;
|
||||||
|
const end = listTokens[listTokens.length - 1].end;
|
||||||
|
|
||||||
|
if (listTokens.length > 1 && listTokens[0].string.match(/\s/)) {
|
||||||
|
start = listTokens[1].start;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { start: start, end: end };
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeMirror.commands.smartListIndent = function(cm: any) {
|
||||||
|
if (cm.getOption('disableInput')) return CodeMirror.Pass;
|
||||||
|
|
||||||
|
const ranges = cm.listSelections();
|
||||||
|
for (let i = 0; i < ranges.length; i++) {
|
||||||
|
const { anchor, head } = ranges[i];
|
||||||
|
|
||||||
|
const line = cm.getLine(anchor.line);
|
||||||
|
|
||||||
|
// This is an actual selection and we should indent
|
||||||
|
if (isSelection(anchor, head) || !isListItem(line)) {
|
||||||
|
cm.execCommand('defaultTab');
|
||||||
|
} else {
|
||||||
|
if (olLineNumber(line)) {
|
||||||
|
const tokens = cm.getLineTokens(anchor.line);
|
||||||
|
const { start, end } = getListSpan(tokens);
|
||||||
|
// Resets numbered list to 1.
|
||||||
|
cm.replaceRange('1. ', { line: anchor.line, ch: start }, { line: anchor.line, ch: end });
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.indentLine(anchor.line, 'add');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CodeMirror.commands.smartListUnindent = function(cm: any) {
|
||||||
|
if (cm.getOption('disableInput')) return CodeMirror.Pass;
|
||||||
|
|
||||||
|
const ranges = cm.listSelections();
|
||||||
|
for (let i = 0; i < ranges.length; i++) {
|
||||||
|
const { anchor, head } = ranges[i];
|
||||||
|
|
||||||
|
const line = cm.getLine(anchor.line);
|
||||||
|
|
||||||
|
// This is an actual selection and we should unindent
|
||||||
|
if (isSelection(anchor, head) || !isListItem(line)) {
|
||||||
|
cm.execCommand('indentLess');
|
||||||
|
} else {
|
||||||
|
const newToken = newListToken(cm, anchor.line);
|
||||||
|
const tokens = cm.getLineTokens(anchor.line);
|
||||||
|
const { start, end } = getListSpan(tokens);
|
||||||
|
|
||||||
|
cm.replaceRange(newToken, { line: anchor.line, ch: start }, { line: anchor.line, ch: end });
|
||||||
|
|
||||||
|
cm.indentLine(anchor.line, 'subtract');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CodeMirror.commands.insertListElement = function(cm: any) {
|
||||||
|
if (cm.getOption('disableInput')) return CodeMirror.Pass;
|
||||||
|
|
||||||
|
const ranges = cm.listSelections();
|
||||||
|
for (let i = 0; i < ranges.length; i++) {
|
||||||
|
const { anchor } = ranges[i];
|
||||||
|
|
||||||
|
const line = cm.getLine(anchor.line);
|
||||||
|
|
||||||
|
if (isEmptyListItem(line)) {
|
||||||
|
const tokens = cm.getLineTokens(anchor.line);
|
||||||
|
// A empty list item with an indent will have whitespace as the first token
|
||||||
|
if (tokens.length > 1 && tokens[0].string.match(/\s/)) {
|
||||||
|
cm.execCommand('smartListUnindent');
|
||||||
|
} else {
|
||||||
|
cm.replaceRange('', { line: anchor.line, ch: 0 }, anchor);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cm.execCommand('newlineAndIndentContinueMarkdownList');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
// Helper functions to sync up scrolling
|
||||||
|
export default function useScrollUtils(CodeMirror: any) {
|
||||||
|
function getScrollHeight(cm: any) {
|
||||||
|
const info = cm.getScrollInfo();
|
||||||
|
const overdraw = cm.state.scrollPastEndPadding ? cm.state.scrollPastEndPadding : '0px';
|
||||||
|
return info.height - info.clientHeight - parseInt(overdraw);
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeMirror.defineExtension('getScrollPercent', function() {
|
||||||
|
const info = this.getScrollInfo();
|
||||||
|
|
||||||
|
return info.top / getScrollHeight(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineExtension('setScrollPercent', function(p: number) {
|
||||||
|
this.scrollTo(null, p * getScrollHeight(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { useState, useEffect, useCallback, useRef } from 'react';
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import TinyMCE from './NoteBody/TinyMCE/TinyMCE';
|
import TinyMCE from './NoteBody/TinyMCE/TinyMCE';
|
||||||
import AceEditor from './NoteBody/AceEditor/AceEditor';
|
import AceEditor from './NoteBody/AceEditor/AceEditor';
|
||||||
|
import CodeMirror from './NoteBody/CodeMirror/CodeMirror';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import MultiNoteActions from '../MultiNoteActions';
|
import MultiNoteActions from '../MultiNoteActions';
|
||||||
import NoteToolbar from '../NoteToolbar/NoteToolbar';
|
import NoteToolbar from '../NoteToolbar/NoteToolbar';
|
||||||
@ -448,6 +449,8 @@ function NoteEditor(props: NoteEditorProps) {
|
|||||||
editor = <TinyMCE {...editorProps}/>;
|
editor = <TinyMCE {...editorProps}/>;
|
||||||
} else if (props.bodyEditor === 'AceEditor') {
|
} else if (props.bodyEditor === 'AceEditor') {
|
||||||
editor = <AceEditor {...editorProps}/>;
|
editor = <AceEditor {...editorProps}/>;
|
||||||
|
} else if (props.bodyEditor === 'CodeMirror') {
|
||||||
|
editor = <CodeMirror {...editorProps}/>;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid editor: ${props.bodyEditor}`);
|
throw new Error(`Invalid editor: ${props.bodyEditor}`);
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,8 @@ const aritimStyle = {
|
|||||||
htmlCodeBorderColor: '#141a21', // Single line code border, and tables
|
htmlCodeBorderColor: '#141a21', // Single line code border, and tables
|
||||||
htmlCodeColor: '#005b47', // Single line code text
|
htmlCodeColor: '#005b47', // Single line code text
|
||||||
|
|
||||||
editorTheme: 'chaos',
|
aceEditorTheme: 'chaos',
|
||||||
|
codeMirrorTheme: 'monokai',
|
||||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||||
|
|
||||||
highlightedColor: '#d3dae3',
|
highlightedColor: '#d3dae3',
|
||||||
|
@ -33,7 +33,8 @@ const darkStyle = {
|
|||||||
htmlCodeBackgroundColor: 'rgb(47, 48, 49)',
|
htmlCodeBackgroundColor: 'rgb(47, 48, 49)',
|
||||||
htmlCodeBorderColor: 'rgb(70, 70, 70)',
|
htmlCodeBorderColor: 'rgb(70, 70, 70)',
|
||||||
|
|
||||||
editorTheme: 'twilight',
|
aceEditorTheme: 'twilight',
|
||||||
|
codeMirrorTheme: 'material-darker',
|
||||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||||
|
|
||||||
highlightedColor: '#0066C7',
|
highlightedColor: '#0066C7',
|
||||||
|
@ -33,7 +33,8 @@ const draculaStyle = {
|
|||||||
htmlCodeBorderColor: '#f8f8f2',
|
htmlCodeBorderColor: '#f8f8f2',
|
||||||
htmlCodeColor: '#50fa7b',
|
htmlCodeColor: '#50fa7b',
|
||||||
|
|
||||||
editorTheme: 'dracula',
|
aceEditorTheme: 'dracula',
|
||||||
|
codeMirrorTheme: 'dracula',
|
||||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,7 +32,8 @@ const lightStyle = {
|
|||||||
htmlCodeBorderColor: 'rgb(220, 220, 220)',
|
htmlCodeBorderColor: 'rgb(220, 220, 220)',
|
||||||
htmlCodeColor: 'rgb(0,0,0)',
|
htmlCodeColor: 'rgb(0,0,0)',
|
||||||
|
|
||||||
editorTheme: 'chrome',
|
aceEditorTheme: 'chrome',
|
||||||
|
codeMirrorTheme: 'default',
|
||||||
codeThemeCss: 'atom-one-light.css',
|
codeThemeCss: 'atom-one-light.css',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,7 +79,8 @@ const nordStyle = {
|
|||||||
htmlCodeBorderColor: nord[2],
|
htmlCodeBorderColor: nord[2],
|
||||||
htmlCodeColor: nord[13],
|
htmlCodeColor: nord[13],
|
||||||
|
|
||||||
editorTheme: 'terminal',
|
aceEditorTheme: 'terminal',
|
||||||
|
codeMirrorTheme: 'nord',
|
||||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,7 +33,8 @@ const solarizedDarkStyle = {
|
|||||||
htmlCodeBorderColor: '#696969',
|
htmlCodeBorderColor: '#696969',
|
||||||
htmlCodeColor: '#fdf6e3',
|
htmlCodeColor: '#fdf6e3',
|
||||||
|
|
||||||
editorTheme: 'twilight',
|
aceEditorTheme: 'twilight',
|
||||||
|
codeMirrorTheme: 'solarized dark',
|
||||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,7 +31,8 @@ const solarizedLightStyle = {
|
|||||||
htmlCodeBorderColor: '#eee8d5',
|
htmlCodeBorderColor: '#eee8d5',
|
||||||
htmlCodeColor: '#002b36',
|
htmlCodeColor: '#002b36',
|
||||||
|
|
||||||
editorTheme: 'tomorrow',
|
aceEditorTheme: 'tomorrow',
|
||||||
|
codeMirrorTheme: 'solarized light',
|
||||||
codeThemeCss: 'atom-one-light.css',
|
codeThemeCss: 'atom-one-light.css',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,6 +12,12 @@
|
|||||||
<link rel="stylesheet" href="node_modules/@fortawesome/fontawesome-free/css/all.min.css">
|
<link rel="stylesheet" href="node_modules/@fortawesome/fontawesome-free/css/all.min.css">
|
||||||
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
|
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
|
||||||
<link rel="stylesheet" href="node_modules/smalltalk/css/smalltalk.css">
|
<link rel="stylesheet" href="node_modules/smalltalk/css/smalltalk.css">
|
||||||
|
<link rel="stylesheet" href="node_modules/codemirror/lib/codemirror.css">
|
||||||
|
<link rel="stylesheet" href="node_modules/codemirror/theme/dracula.css">
|
||||||
|
<link rel="stylesheet" href="node_modules/codemirror/theme/monokai.css">
|
||||||
|
<link rel="stylesheet" href="node_modules/codemirror/theme/solarized.css">
|
||||||
|
<link rel="stylesheet" href="node_modules/codemirror/theme/nord.css">
|
||||||
|
<link rel="stylesheet" href="node_modules/codemirror/theme/material-darker.css">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.smalltalk {
|
.smalltalk {
|
||||||
@ -40,4 +46,4 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -102,6 +102,7 @@
|
|||||||
"base64-stream": "^1.0.0",
|
"base64-stream": "^1.0.0",
|
||||||
"chokidar": "^3.0.0",
|
"chokidar": "^3.0.0",
|
||||||
"clean-html": "^1.5.0",
|
"clean-html": "^1.5.0",
|
||||||
|
"codemirror": "^5.54.0",
|
||||||
"color": "^3.1.2",
|
"color": "^3.1.2",
|
||||||
"compare-versions": "^3.2.1",
|
"compare-versions": "^3.2.1",
|
||||||
"countable": "^3.0.1",
|
"countable": "^3.0.1",
|
||||||
|
@ -169,4 +169,34 @@ a {
|
|||||||
@keyframes rotate {
|
@keyframes rotate {
|
||||||
from {transform: rotate(0deg);}
|
from {transform: rotate(0deg);}
|
||||||
to {transform: rotate(360deg);}
|
to {transform: rotate(360deg);}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* These must be important to prevent the codemirror defaults from taking over*/
|
||||||
|
.CodeMirror {
|
||||||
|
font-family: monospace;
|
||||||
|
height: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
color: inherit !important;
|
||||||
|
background-color: inherit !important;
|
||||||
|
position: absolute !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-header-1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-header-2 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-header-3 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-header-4, .cm-header-5, .cm-header-6 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4, .cm-header-5, .cm-header-6 {
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
@ -4,6 +4,10 @@ const MarkdownIt = require('markdown-it');
|
|||||||
const { setupLinkify } = require('lib/joplin-renderer');
|
const { setupLinkify } = require('lib/joplin-renderer');
|
||||||
const removeMarkdown = require('remove-markdown');
|
const removeMarkdown = require('remove-markdown');
|
||||||
|
|
||||||
|
// Taken from codemirror/addon/edit/continuelist.js
|
||||||
|
const listRegex = /^(\s*)([*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
|
||||||
|
const emptyListRegex = /^(\s*)([*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
|
||||||
|
|
||||||
const markdownUtils = {
|
const markdownUtils = {
|
||||||
// Not really escaping because that's not supported by marked.js
|
// Not really escaping because that's not supported by marked.js
|
||||||
escapeLinkText(text) {
|
escapeLinkText(text) {
|
||||||
@ -61,9 +65,25 @@ const markdownUtils = {
|
|||||||
return output;
|
return output;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// The match results has 5 items
|
||||||
|
// Full match array is
|
||||||
|
// [Full match, whitespace, list token, ol line number, whitespace following token]
|
||||||
olLineNumber(line) {
|
olLineNumber(line) {
|
||||||
const match = line.match(/^(\d+)\.(\s.*|)$/);
|
const match = line.match(listRegex);
|
||||||
return match ? Number(match[1]) : 0;
|
return match ? Number(match[3]) : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
extractListToken(line) {
|
||||||
|
const match = line.match(listRegex);
|
||||||
|
return match ? match[2] : '';
|
||||||
|
},
|
||||||
|
|
||||||
|
isListItem(line) {
|
||||||
|
return listRegex.test(line);
|
||||||
|
},
|
||||||
|
|
||||||
|
isEmptyListItem(line) {
|
||||||
|
return emptyListRegex.test(line);
|
||||||
},
|
},
|
||||||
|
|
||||||
createMarkdownTable(headers, rows) {
|
createMarkdownTable(headers, rows) {
|
||||||
|
@ -347,6 +347,14 @@ class Setting extends BaseModel {
|
|||||||
appTypes: ['desktop'],
|
appTypes: ['desktop'],
|
||||||
label: () => _('Auto-pair braces, parenthesis, quotations, etc.'),
|
label: () => _('Auto-pair braces, parenthesis, quotations, etc.'),
|
||||||
},
|
},
|
||||||
|
'editor.betaCodeMirror': {
|
||||||
|
value: false,
|
||||||
|
type: Setting.TYPE_BOOL,
|
||||||
|
public: true,
|
||||||
|
section: 'note',
|
||||||
|
appTypes: ['desktop'],
|
||||||
|
label: () => _('Use CodeMirror as the code editor (WARNING: BETA).'),
|
||||||
|
},
|
||||||
'notes.sortOrder.reverse': { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
|
'notes.sortOrder.reverse': { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
|
||||||
'folders.sortOrder.field': {
|
'folders.sortOrder.field': {
|
||||||
value: 'title',
|
value: 'title',
|
||||||
@ -499,7 +507,7 @@ class Setting extends BaseModel {
|
|||||||
section: 'appearance',
|
section: 'appearance',
|
||||||
label: () => _('Editor font family'),
|
label: () => _('Editor font family'),
|
||||||
description: () =>
|
description: () =>
|
||||||
_('This must be *monospace* font or it will not work properly. If the font ' +
|
_('This should be a *monospace* font or some elements will render incorrectly. If the font ' +
|
||||||
'is incorrect or empty, it will default to a generic monospace font.'),
|
'is incorrect or empty, it will default to a generic monospace font.'),
|
||||||
},
|
},
|
||||||
'style.sidebar.width': { value: 150, minimum: 80, maximum: 400, type: Setting.TYPE_INT, public: false, appTypes: ['desktop'] },
|
'style.sidebar.width': { value: 150, minimum: 80, maximum: 400, type: Setting.TYPE_INT, public: false, appTypes: ['desktop'] },
|
||||||
|
Loading…
Reference in New Issue
Block a user