mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +02:00
This commit is contained in:
parent
652748f969
commit
fbe966903b
@ -104,12 +104,6 @@ ElectronClient/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
ElectronClient/gui/NoteEditor/commands/focusElementNoteTitle.js
|
||||
ElectronClient/gui/NoteEditor/commands/showLocalSearch.js
|
||||
ElectronClient/gui/NoteEditor/commands/showRevisions.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/styles/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/types.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
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -97,12 +97,6 @@ ElectronClient/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
ElectronClient/gui/NoteEditor/commands/focusElementNoteTitle.js
|
||||
ElectronClient/gui/NoteEditor/commands/showLocalSearch.js
|
||||
ElectronClient/gui/NoteEditor/commands/showRevisions.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/styles/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/types.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
|
||||
|
@ -1042,11 +1042,9 @@ class Application extends BaseApplication {
|
||||
// https://github.com/laurent22/joplin/issues/155
|
||||
|
||||
const css = `.CodeMirror * { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||
const ace_css = `.ace_editor * { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||
const styleTag = document.createElement('style');
|
||||
styleTag.type = 'text/css';
|
||||
styleTag.appendChild(document.createTextNode(css));
|
||||
styleTag.appendChild(document.createTextNode(ace_css));
|
||||
document.head.appendChild(styleTag);
|
||||
}
|
||||
|
||||
|
@ -434,7 +434,7 @@ class MainScreenComponent extends React.Component {
|
||||
// A bit of a hack, but for now don't allow changing code view
|
||||
// while a note is being saved as it will cause a problem with
|
||||
// TinyMCE because it won't have time to send its content before
|
||||
// being switch to Ace Editor.
|
||||
// being switch to the Code Editor.
|
||||
if (this.props.hasNotesBeingSaved) return;
|
||||
Setting.toggle('editor.codeView');
|
||||
},
|
||||
@ -468,8 +468,7 @@ class MainScreenComponent extends React.Component {
|
||||
const noteContentPropertiesDialogOptions = this.state.noteContentPropertiesDialogOptions;
|
||||
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
|
||||
|
||||
const codeEditor = Setting.value('editor.betaCodeMirror') ? 'CodeMirror' : 'AceEditor';
|
||||
const bodyEditor = this.props.settingEditorCodeView ? codeEditor : 'TinyMCE';
|
||||
const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
|
@ -1,654 +0,0 @@
|
||||
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 { textOffsetToCursorPosition, useScrollHandler, useRootWidth, usePrevious, lineLeftSpaces, selectionRange, selectionRangeCurrentLine, selectionRangePreviousLine, currentTextOffset, textOffsetSelection, selectedText } from './utils';
|
||||
import useListIdent from './utils/useListIdent';
|
||||
import Toolbar from './Toolbar';
|
||||
import styles_ from './styles';
|
||||
import { RenderedBody, defaultRenderedBody } from './utils/types';
|
||||
|
||||
const AceEditorReact = require('react-ace').default;
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
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');
|
||||
|
||||
require('brace/mode/markdown');
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
// https://highlightjs.org/static/demo/
|
||||
require('brace/theme/chrome');
|
||||
require('brace/theme/solarized_light');
|
||||
require('brace/theme/solarized_dark');
|
||||
require('brace/theme/twilight');
|
||||
require('brace/theme/dracula');
|
||||
require('brace/theme/chaos');
|
||||
require('brace/theme/tomorrow');
|
||||
require('brace/keybinding/vim');
|
||||
require('brace/keybinding/emacs');
|
||||
require('brace/theme/terminal');
|
||||
|
||||
// TODO: Could not get below code to work
|
||||
|
||||
// @ts-ignore Ace global variable
|
||||
// const aceGlobal = (ace as any);
|
||||
|
||||
// class CustomHighlightRules extends aceGlobal.acequire(
|
||||
// 'ace/mode/markdown_highlight_rules'
|
||||
// ).MarkdownHighlightRules {
|
||||
// constructor() {
|
||||
// super();
|
||||
// if (Setting.value('markdown.plugin.mark')) {
|
||||
// this.$rules.start.push({
|
||||
// // This is actually a highlight `mark`, but Ace has no token name for
|
||||
// // this so we made up our own. Reference for common tokens here:
|
||||
// // https://github.com/ajaxorg/ace/wiki/Creating-or-Extending-an-Edit-Mode#common-tokens
|
||||
// token: 'highlight_mark',
|
||||
// regex: '==[^ ](?:.*?[^ ])?==',
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// /* eslint-disable-next-line no-undef */
|
||||
// class CustomMdMode extends aceGlobal.acequire('ace/mode/markdown').Mode {
|
||||
// constructor() {
|
||||
// super();
|
||||
// this.HighlightRules = CustomHighlightRules;
|
||||
// }
|
||||
// }
|
||||
|
||||
function markupRenderOptions(override: any = null) {
|
||||
return { ...override };
|
||||
}
|
||||
|
||||
function AceEditor(props: NoteBodyEditorProps, ref: any) {
|
||||
const styles = styles_(props);
|
||||
|
||||
const [renderedBody, setRenderedBody] = useState<RenderedBody>(defaultRenderedBody()); // Viewer content
|
||||
const [editor, setEditor] = useState(null);
|
||||
const [webviewReady, setWebviewReady] = useState(false);
|
||||
|
||||
const previousRenderedBody = usePrevious(renderedBody);
|
||||
const previousSearchMarkers = usePrevious(props.searchMarkers);
|
||||
const previousContentKey = usePrevious(props.contentKey);
|
||||
|
||||
const editorRef = useRef(null);
|
||||
editorRef.current = editor;
|
||||
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 rootWidth = useRootWidth({ rootRef });
|
||||
|
||||
const { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll } = useScrollHandler(editor, webviewRef, props.onScroll);
|
||||
|
||||
useListIdent({ editor });
|
||||
|
||||
const aceEditor_change = useCallback((newBody: string) => {
|
||||
// Throw an error early to know what part of the code set the body to the
|
||||
// wrong value. Otherwise it will trigger an error somewhere deep in React-Ace
|
||||
// which will be hard to debug.
|
||||
if (typeof newBody !== 'string') throw new Error('Body is not a string');
|
||||
props_onChangeRef.current({ changeId: null, content: newBody });
|
||||
}, []);
|
||||
|
||||
const wrapSelectionWithStrings = useCallback((string1: string, string2 = '', defaultText = '', replacementText: string = null, byLine = false) => {
|
||||
if (!editor) return;
|
||||
|
||||
const selection = textOffsetSelection(selectionRange(editor), props.content);
|
||||
|
||||
let newBody = props.content;
|
||||
|
||||
if (selection && selection.start !== selection.end) {
|
||||
const selectedLines = replacementText !== null ? replacementText : props.content.substr(selection.start, selection.end - selection.start);
|
||||
const selectedStrings = byLine ? selectedLines.split(/\r?\n/) : [selectedLines];
|
||||
|
||||
newBody = props.content.substr(0, selection.start);
|
||||
|
||||
let startCursorPos, endCursorPos;
|
||||
|
||||
for (let i = 0; i < selectedStrings.length; i++) {
|
||||
if (byLine == false) {
|
||||
const start = selectedStrings[i].search(/[^\s]/);
|
||||
const end = selectedStrings[i].search(/[^\s](?=[\s]*$)/);
|
||||
newBody += selectedStrings[i].substr(0, start) + string1 + selectedStrings[i].substr(start, end - start + 1) + string2 + selectedStrings[i].substr(end + 1);
|
||||
// Getting position for correcting offset in highlighted text when surrounded by white spaces
|
||||
startCursorPos = textOffsetToCursorPosition(selection.start + start, newBody);
|
||||
endCursorPos = textOffsetToCursorPosition(selection.start + end + 1, newBody);
|
||||
|
||||
} else { newBody += string1 + selectedStrings[i] + string2; }
|
||||
|
||||
}
|
||||
|
||||
newBody += props.content.substr(selection.end);
|
||||
|
||||
const r = selectionRange(editor);
|
||||
|
||||
// Because some insertion strings will have newlines, we'll need to account for them
|
||||
const str1Split = string1.split(/\r?\n/);
|
||||
|
||||
// Add the number of newlines to the row
|
||||
// and add the length of the final line to the column (for strings with no newlines this is the string length)
|
||||
|
||||
let newRange: any = {};
|
||||
if (!byLine) {
|
||||
// Correcting offset in Highlighted text when surrounded by white spaces
|
||||
newRange = {
|
||||
start: {
|
||||
row: startCursorPos.row,
|
||||
column: startCursorPos.column + string1.length,
|
||||
},
|
||||
end: {
|
||||
row: endCursorPos.row,
|
||||
column: endCursorPos.column + string1.length,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
newRange = {
|
||||
start: {
|
||||
row: r.start.row + str1Split.length - 1,
|
||||
column: r.start.column + str1Split[str1Split.length - 1].length,
|
||||
},
|
||||
end: {
|
||||
row: r.end.row + str1Split.length - 1,
|
||||
column: r.end.column + str1Split[str1Split.length - 1].length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (replacementText !== null) {
|
||||
const diff = replacementText.length - (selection.end - selection.start);
|
||||
newRange.end.column += diff;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const range = selectionRange(editor);
|
||||
range.setStart(newRange.start.row, newRange.start.column);
|
||||
range.setEnd(newRange.end.row, newRange.end.column);
|
||||
editor.getSession().getSelection().setSelectionRange(range, false);
|
||||
editor.focus();
|
||||
}, 10);
|
||||
} else {
|
||||
const middleText = replacementText !== null ? replacementText : defaultText;
|
||||
const textOffset = currentTextOffset(editor, props.content);
|
||||
const s1 = props.content.substr(0, textOffset);
|
||||
const s2 = props.content.substr(textOffset);
|
||||
newBody = s1 + string1 + middleText + string2 + s2;
|
||||
|
||||
const p = textOffsetToCursorPosition(textOffset + string1.length, newBody);
|
||||
const newRange = {
|
||||
start: { row: p.row, column: p.column },
|
||||
end: { row: p.row, column: p.column + middleText.length },
|
||||
};
|
||||
|
||||
// BUG!! If replacementText contains newline characters, the logic
|
||||
// to select the new text will not work.
|
||||
|
||||
setTimeout(() => {
|
||||
if (middleText && newRange) {
|
||||
const range = selectionRange(editor);
|
||||
range.setStart(newRange.start.row, newRange.start.column);
|
||||
range.setEnd(newRange.end.row, newRange.end.column);
|
||||
editor.getSession().getSelection().setSelectionRange(range, false);
|
||||
} else {
|
||||
for (let i = 0; i < string1.length; i++) {
|
||||
editor.getSession().getSelection().moveCursorRight();
|
||||
}
|
||||
}
|
||||
editor.focus();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
aceEditor_change(newBody);
|
||||
}, [editor, props.content, aceEditor_change]);
|
||||
|
||||
const addListItem = useCallback((string1, string2 = '', defaultText = '', byLine = false) => {
|
||||
let newLine = '\n';
|
||||
const range = selectionRange(editor);
|
||||
if (!range || (range.start.row === range.end.row && !selectionRangeCurrentLine(range, props.content))) {
|
||||
newLine = '';
|
||||
}
|
||||
wrapSelectionWithStrings(newLine + string1, string2, defaultText, null, byLine);
|
||||
}, [wrapSelectionWithStrings, props.content, editor]);
|
||||
|
||||
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 (!editor) return false;
|
||||
|
||||
reg.logger().debug('AceEditor: execCommand', cmd);
|
||||
|
||||
let commandProcessed = true;
|
||||
|
||||
if (cmd.name === 'dropItems') {
|
||||
if (cmd.value.type === 'notes') {
|
||||
wrapSelectionWithStrings('', '', '', cmd.value.markdownTags.join('\n'));
|
||||
} else if (cmd.value.type === 'files') {
|
||||
const newBody = await commandAttachFileToBody(props.content, cmd.value.paths, { createFileURL: !!cmd.value.createFileURL });
|
||||
if (newBody) aceEditor_change(newBody);
|
||||
} else {
|
||||
reg.logger().warn('AceEditor: unsupported drop item: ', cmd);
|
||||
}
|
||||
} else if (cmd.name === 'focus') {
|
||||
editor.focus();
|
||||
} else {
|
||||
commandProcessed = false;
|
||||
}
|
||||
|
||||
if (!commandProcessed) {
|
||||
const commands: any = {
|
||||
textBold: () => wrapSelectionWithStrings('**', '**', _('strong text')),
|
||||
textItalic: () => wrapSelectionWithStrings('*', '*', _('emphasised text')),
|
||||
textLink: async () => {
|
||||
const url = await dialogs.prompt(_('Insert Hyperlink'));
|
||||
if (url) wrapSelectionWithStrings('[', `](${url})`);
|
||||
},
|
||||
textCode: () => {
|
||||
const selection = textOffsetSelection(selectionRange(editor), props.content);
|
||||
const string = props.content.substr(selection.start, selection.end - selection.start);
|
||||
|
||||
// Look for newlines
|
||||
const match = string.match(/\r?\n/);
|
||||
|
||||
if (match && match.length > 0) {
|
||||
if (string.startsWith('```') && string.endsWith('```')) {
|
||||
wrapSelectionWithStrings('', '', '', string.substr(4, selection.end - selection.start - 8));
|
||||
} else {
|
||||
wrapSelectionWithStrings(`\`\`\`${match[0]}`, `${match[0]}\`\`\``);
|
||||
}
|
||||
} else {
|
||||
wrapSelectionWithStrings('`', '`', '');
|
||||
}
|
||||
},
|
||||
insertText: (value: any) => wrapSelectionWithStrings(value),
|
||||
attachFile: async () => {
|
||||
const selection = textOffsetSelection(selectionRange(editor), props.content);
|
||||
const newBody = await commandAttachFileToBody(props.content, null, { position: selection ? selection.start : 0 });
|
||||
if (newBody) aceEditor_change(newBody);
|
||||
},
|
||||
textNumberedList: () => {
|
||||
const selection = selectionRange(editor);
|
||||
let bulletNumber = markdownUtils.olLineNumber(selectionRangeCurrentLine(selection, props.content));
|
||||
if (!bulletNumber) bulletNumber = markdownUtils.olLineNumber(selectionRangePreviousLine(selection, props.content));
|
||||
if (!bulletNumber) bulletNumber = 0;
|
||||
addListItem(`${bulletNumber + 1}. `, '', _('List item'), true);
|
||||
},
|
||||
textBulletedList: () => addListItem('- ', '', _('List item'), true),
|
||||
textCheckbox: () => addListItem('- [ ] ', '', _('List item'), true),
|
||||
textHeading: () => addListItem('## ','','', true),
|
||||
textHorizontalRule: () => addListItem('* * *'),
|
||||
};
|
||||
|
||||
if (commands[cmd.name]) {
|
||||
commands[cmd.name](cmd.value);
|
||||
} else {
|
||||
reg.logger().warn('AceEditor: unsupported Joplin command: ', cmd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}, [editor, props.content, addListItem, wrapSelectionWithStrings, selectionRangeCurrentLine, aceEditor_change, setEditorPercentScroll, setViewerPercentScroll, resetScroll, renderedBody]);
|
||||
|
||||
const onEditorPaste = useCallback(async (event: any = null) => {
|
||||
const resourceMds = await handlePasteEvent(event);
|
||||
if (!resourceMds.length) return;
|
||||
wrapSelectionWithStrings('', '', resourceMds.join('\n'));
|
||||
}, [wrapSelectionWithStrings]);
|
||||
|
||||
const editorCutText = useCallback(() => {
|
||||
const text = selectedText(selectionRange(editor), props.content);
|
||||
if (!text) return;
|
||||
|
||||
clipboard.writeText(text);
|
||||
|
||||
const s = textOffsetSelection(selectionRange(editor), props.content);
|
||||
if (!s || s.start === s.end) return;
|
||||
|
||||
const s1 = props.content.substr(0, s.start);
|
||||
const s2 = props.content.substr(s.end);
|
||||
|
||||
aceEditor_change(s1 + s2);
|
||||
|
||||
setTimeout(() => {
|
||||
const range = selectionRange(editor);
|
||||
range.setStart(range.start.row, range.start.column);
|
||||
range.setEnd(range.start.row, range.start.column);
|
||||
editor.getSession().getSelection().setSelectionRange(range, false);
|
||||
editor.focus();
|
||||
}, 10);
|
||||
}, [props.content, editor, aceEditor_change]);
|
||||
|
||||
function clipboardText() {
|
||||
return clipboard.readText() ? clipboard.readText() : clipboard.readHTML();
|
||||
}
|
||||
|
||||
const editorCopyText = useCallback(() => {
|
||||
const text = selectedText(selectionRange(editor), props.content);
|
||||
clipboard.writeText(text);
|
||||
}, [props.content, editor]);
|
||||
|
||||
const editorPasteText = useCallback(() => {
|
||||
wrapSelectionWithStrings(clipboardText(), '', '', '');
|
||||
}, [wrapSelectionWithStrings]);
|
||||
|
||||
const onEditorContextMenu = useCallback(() => {
|
||||
const menu = new Menu();
|
||||
|
||||
const hasSelectedText = !!selectedText(selectionRange(editor), props.content);
|
||||
const currentClipboardText = clipboardText();
|
||||
|
||||
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 (currentClipboardText) {
|
||||
editorPasteText();
|
||||
} else {
|
||||
// To handle pasting images
|
||||
onEditorPaste();
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste, editor]);
|
||||
|
||||
function aceEditor_load(editor: any) {
|
||||
setEditor(editor);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
const cancelledKeys = [];
|
||||
const letters = ['F', 'T', 'P', 'Q', 'L', ',', 'G', 'K'];
|
||||
for (let i = 0; i < letters.length; i++) {
|
||||
const l = letters[i];
|
||||
cancelledKeys.push(`Ctrl+${l}`);
|
||||
cancelledKeys.push(`Command+${l}`);
|
||||
}
|
||||
cancelledKeys.push('Alt+E');
|
||||
cancelledKeys.push('Command+Shift+L');
|
||||
cancelledKeys.push('Ctrl+Shift+L');
|
||||
|
||||
for (let i = 0; i < cancelledKeys.length; i++) {
|
||||
const k = cancelledKeys[i];
|
||||
editor.commands.bindKey(k, () => {
|
||||
// HACK: Ace doesn't seem to provide a way to override its shortcuts, but throwing
|
||||
// an exception from this undocumented function seems to cancel it without any
|
||||
// side effect.
|
||||
// https://stackoverflow.com/questions/36075846
|
||||
throw new Error(`HACK: Overriding Ace Editor shortcut: ${k}`);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelector('#note-editor').addEventListener('paste', onEditorPaste, true);
|
||||
document.querySelector('#note-editor').addEventListener('contextmenu', onEditorContextMenu);
|
||||
|
||||
// Disable Markdown auto-completion (eg. auto-adding a dash after a line with a dash.
|
||||
// https://github.com/ajaxorg/ace/issues/2754
|
||||
// @ts-ignore: Keep the function signature as-is despite unusued arguments
|
||||
editor.getSession().getMode().getNextLineIndent = function(state: any, line: string) {
|
||||
const leftSpaces = lineLeftSpaces(line);
|
||||
const lineNoLeftSpaces = line.trimLeft();
|
||||
|
||||
if (lineNoLeftSpaces.indexOf('- [ ] ') === 0 || lineNoLeftSpaces.indexOf('- [x] ') === 0 || lineNoLeftSpaces.indexOf('- [X] ') === 0) return `${leftSpaces}- [ ] `;
|
||||
if (lineNoLeftSpaces.indexOf('- ') === 0) return `${leftSpaces}- `;
|
||||
if (lineNoLeftSpaces.indexOf('* ') === 0 && line.trim() !== '* * *') return `${leftSpaces}* `;
|
||||
|
||||
const bulletNumber = markdownUtils.olLineNumber(lineNoLeftSpaces);
|
||||
if (bulletNumber) return `${leftSpaces + (bulletNumber + 1)}. `;
|
||||
|
||||
return this.$getIndent(line);
|
||||
};
|
||||
|
||||
return () => {
|
||||
document.querySelector('#note-editor').removeEventListener('paste', onEditorPaste, true);
|
||||
document.querySelector('#note-editor').removeEventListener('contextmenu', onEditorContextMenu);
|
||||
};
|
||||
}, [editor, onEditorPaste, onEditorContextMenu]);
|
||||
|
||||
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);
|
||||
aceEditor_change(newBody);
|
||||
} else if (msg === 'percentScroll') {
|
||||
setEditorPercentScroll(arg0);
|
||||
} else {
|
||||
props.onMessage(event);
|
||||
}
|
||||
}, [props.onMessage, props.content, aceEditor_change, 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 (!editor) return;
|
||||
|
||||
if (contentKeyHasChangedRef.current) {
|
||||
// editor.getSession().setMode(new CustomMdMode());
|
||||
const undoManager = editor.getSession().getUndoManager();
|
||||
undoManager.reset();
|
||||
editor.getSession().setUndoManager(undoManager);
|
||||
}
|
||||
}, [props.content, editor]);
|
||||
|
||||
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')) {
|
||||
// Note: Ideally we'd set the display to "none" to take the editor out
|
||||
// of the DOM but if we do that, certain things won't work, in particular
|
||||
// things related to scroll, which are based on the editor.
|
||||
|
||||
// Note that the below hack doesn't work and causes a bug in this case:
|
||||
// - Put Ace Editor in viewer-only mode
|
||||
// - Go to WYSIWYG editor
|
||||
// - Create new to-do - set title only
|
||||
// - Switch to Code View
|
||||
// - Switch layout and type something
|
||||
// => Text editor layout is broken and text is off-screen
|
||||
|
||||
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() {
|
||||
// Need to hard-code the editor width, otherwise various bugs pops up
|
||||
let width = 0;
|
||||
if (props.visiblePanes.includes('editor')) {
|
||||
width = !props.visiblePanes.includes('viewer') ? rootWidth : Math.floor(rootWidth / 2);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={cellEditorStyle}>
|
||||
<AceEditorReact
|
||||
value={props.content}
|
||||
mode={props.contentMarkupLanguage === Note.MARKUP_LANGUAGE_HTML ? 'text' : 'markdown'}
|
||||
theme={styles.editor.aceEditorTheme}
|
||||
style={styles.editor}
|
||||
width={`${width}px`}
|
||||
fontSize={styles.editor.fontSize}
|
||||
showGutter={false}
|
||||
readOnly={editorReadOnly}
|
||||
name="note-editor"
|
||||
wrapEnabled={true}
|
||||
onScroll={editor_scroll}
|
||||
onChange={aceEditor_change}
|
||||
showPrintMargin={false}
|
||||
onLoad={aceEditor_load}
|
||||
// Enable/Disable the autoclosing braces
|
||||
setOptions={
|
||||
{
|
||||
behavioursEnabled: Setting.value('editor.autoMatchingBraces'),
|
||||
useSoftTabs: false,
|
||||
}
|
||||
}
|
||||
// Disable warning: "Automatically scrolling cursor into view after
|
||||
// selection change this will be disabled in the next version set
|
||||
// editor.$blockScrolling = Infinity to disable this message"
|
||||
editorProps={{ $blockScrolling: Infinity }}
|
||||
// This is buggy (gets outside the container)
|
||||
highlightActiveLine={false}
|
||||
keyboardHandler={props.keyboardMode}
|
||||
/>
|
||||
</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(AceEditor);
|
||||
|
@ -1,48 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import CommandService from '../../../../lib/services/CommandService';
|
||||
|
||||
const ToolbarBase = require('../../../Toolbar.min.js');
|
||||
const { buildStyle, themeStyle } = require('lib/theme');
|
||||
|
||||
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);
|
||||
|
||||
const cmdService = CommandService.instance();
|
||||
|
||||
const toolbarItems = [
|
||||
cmdService.commandToToolbarButton('textBold'),
|
||||
cmdService.commandToToolbarButton('textItalic'),
|
||||
{ type: 'separator' },
|
||||
cmdService.commandToToolbarButton('textLink'),
|
||||
cmdService.commandToToolbarButton('textCode'),
|
||||
cmdService.commandToToolbarButton('attachFile'),
|
||||
{ type: 'separator' },
|
||||
cmdService.commandToToolbarButton('textNumberedList'),
|
||||
cmdService.commandToToolbarButton('textBulletedList'),
|
||||
cmdService.commandToToolbarButton('textCheckbox'),
|
||||
cmdService.commandToToolbarButton('textHeading'),
|
||||
cmdService.commandToToolbarButton('textHorizontalRule'),
|
||||
cmdService.commandToToolbarButton('insertDateTime'),
|
||||
];
|
||||
|
||||
return <ToolbarBase disabled={props.disabled} style={styles.root} items={toolbarItems} />;
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
import { NoteBodyEditorProps } from '../../../utils/types';
|
||||
const { buildStyle } = require('lib/theme');
|
||||
|
||||
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,
|
||||
aceEditorTheme: theme.aceEditorTheme, // Defined in theme.js
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
@ -1,219 +0,0 @@
|
||||
import { useState, 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.row) {
|
||||
pos += cursorPos.column;
|
||||
break;
|
||||
} else {
|
||||
pos += noteLines[i].length;
|
||||
}
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
export function currentTextOffset(editor: any, body: string) {
|
||||
return cursorPositionToTextOffset(editor.getCursorPosition(), body);
|
||||
}
|
||||
|
||||
export function rangeToTextOffsets(range: any, body: string) {
|
||||
return {
|
||||
start: cursorPositionToTextOffset(range.start, body),
|
||||
end: cursorPositionToTextOffset(range.end, body),
|
||||
};
|
||||
}
|
||||
|
||||
export function textOffsetSelection(selectionRange: any, body: string) {
|
||||
return selectionRange && body ? rangeToTextOffsets(selectionRange, body) : null;
|
||||
}
|
||||
|
||||
export function selectedText(selectionRange: any, body: string) {
|
||||
const selection = textOffsetSelection(selectionRange, body);
|
||||
if (!selection || selection.start === selection.end) return '';
|
||||
|
||||
return body.substr(selection.start, selection.end - selection.start);
|
||||
}
|
||||
|
||||
export function selectionRange(editor:any) {
|
||||
const ranges = editor.getSelection().getAllRanges();
|
||||
return ranges && ranges.length ? ranges[0] : null;
|
||||
}
|
||||
|
||||
export function textOffsetToCursorPosition(offset: number, body: string) {
|
||||
const lines = body.split('\n');
|
||||
let row = 0;
|
||||
let currentOffset = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (currentOffset + line.length >= offset) {
|
||||
return {
|
||||
row: row,
|
||||
column: offset - currentOffset,
|
||||
};
|
||||
}
|
||||
|
||||
row++;
|
||||
currentOffset += line.length + 1;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function lineAtRow(body: string, row: number) {
|
||||
if (!body) return '';
|
||||
const lines = body.split('\n');
|
||||
if (row < 0 || row >= lines.length) return '';
|
||||
return lines[row];
|
||||
}
|
||||
|
||||
export function selectionRangeCurrentLine(selectionRange: any, body: string) {
|
||||
if (!selectionRange) return '';
|
||||
return lineAtRow(body, selectionRange.start.row);
|
||||
}
|
||||
|
||||
export function selectionRangePreviousLine(selectionRange: any, body: string) {
|
||||
if (!selectionRange) return '';
|
||||
return lineAtRow(body, selectionRange.start.row - 1);
|
||||
}
|
||||
|
||||
export function lineLeftSpaces(line: string) {
|
||||
let output = '';
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
if ([' ', '\t'].indexOf(line[i]) >= 0) {
|
||||
output += line[i];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function usePrevious(value: any): any {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
export function useScrollHandler(editor: any, webviewRef: any, onScroll: Function) {
|
||||
const editorMaxScrollTop_ = useRef(0);
|
||||
const restoreScrollTop_ = useRef<any>(null);
|
||||
const ignoreNextEditorScrollEvent_ = useRef(false);
|
||||
const scrollTimeoutId_ = useRef<any>(null);
|
||||
|
||||
// TODO: Below is not needed anymore????
|
||||
//
|
||||
// this.editorMaxScrollTop_ = 0;
|
||||
// // HACK: To go around a bug in Ace editor, we first set the scroll position to 1
|
||||
// // and then (in the renderer callback) to the value we actually need. The first
|
||||
// // operation helps clear the scroll position cache. See:
|
||||
// //
|
||||
// this.editorSetScrollTop(1);
|
||||
// this.restoreScrollTop_ = 0;
|
||||
|
||||
const editorSetScrollTop = useCallback((v) => {
|
||||
if (!editor) return;
|
||||
editor.getSession().setScrollTop(v);
|
||||
}, [editor]);
|
||||
|
||||
// Complicated but reliable method to get editor content height
|
||||
// https://github.com/ajaxorg/ace/issues/2046
|
||||
const onAfterEditorRender = useCallback(() => {
|
||||
const r = editor.renderer;
|
||||
editorMaxScrollTop_.current = Math.max(0, r.layerConfig.maxHeight - r.$size.scrollerHeight);
|
||||
|
||||
if (restoreScrollTop_.current !== null) {
|
||||
editorSetScrollTop(restoreScrollTop_.current);
|
||||
restoreScrollTop_.current = null;
|
||||
}
|
||||
}, [editor, editorSetScrollTop]);
|
||||
|
||||
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;
|
||||
editorSetScrollTop(p * editorMaxScrollTop_.current);
|
||||
scheduleOnScroll({ percent: p });
|
||||
}, [editorSetScrollTop, 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;
|
||||
}
|
||||
|
||||
const m = editorMaxScrollTop_.current;
|
||||
const percent = m ? editor.getSession().getScrollTop() / m : 0;
|
||||
|
||||
setViewerPercentScroll(percent);
|
||||
}, [editor, setViewerPercentScroll]);
|
||||
|
||||
const resetScroll = useCallback(() => {
|
||||
if (!editor) return;
|
||||
|
||||
// Ace Editor caches scroll values, which makes
|
||||
// it hard to reset the scroll position, so we
|
||||
// need to use this hack.
|
||||
// https://github.com/ajaxorg/ace/issues/2195
|
||||
editor.session.$scrollTop = -1;
|
||||
editor.session.$scrollLeft = -1;
|
||||
editor.renderer.scrollTop = -1;
|
||||
editor.renderer.scrollLeft = -1;
|
||||
editor.renderer.scrollBarV.scrollTop = -1;
|
||||
editor.renderer.scrollBarH.scrollLeft = -1;
|
||||
editor.session.setScrollTop(0);
|
||||
editor.session.setScrollLeft(0);
|
||||
}, [editorSetScrollTop, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
editor.renderer.on('afterRender', onAfterEditorRender);
|
||||
|
||||
return () => {
|
||||
editor.renderer.off('afterRender', onAfterEditorRender);
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
return { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll };
|
||||
}
|
||||
|
||||
export function useRootWidth(dependencies:any) {
|
||||
const { rootRef } = dependencies;
|
||||
|
||||
const [rootWidth, setRootWidth] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootRef.current) return;
|
||||
|
||||
if (rootWidth !== rootRef.current.offsetWidth) setRootWidth(rootRef.current.offsetWidth);
|
||||
});
|
||||
|
||||
return rootWidth;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
export interface RenderedBody {
|
||||
html: string;
|
||||
pluginAssets: any[];
|
||||
}
|
||||
|
||||
export function defaultRenderedBody(): RenderedBody {
|
||||
return {
|
||||
html: '',
|
||||
pluginAssets: [],
|
||||
};
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { selectionRange } from './index';
|
||||
const markdownUtils = require('lib/markdownUtils');
|
||||
|
||||
// The line that contains only `- ` is
|
||||
// recognized as a heading in Ace.
|
||||
function hyphenEmptyListItem(tokens: any[]) {
|
||||
return (
|
||||
tokens.length === 2 &&
|
||||
tokens[0].type === 'markup.heading.2' &&
|
||||
tokens[0].value === '-' &&
|
||||
tokens[1].type === 'text.xml' &&
|
||||
tokens[1].value === ' '
|
||||
);
|
||||
}
|
||||
|
||||
// Returns tokens of the line if it starts with a 'markup.list' token.
|
||||
function listTokens(editor: any, row: number) {
|
||||
const tokens = editor.session.getTokens(row);
|
||||
if (
|
||||
!(tokens.length > 0 && tokens[0].type === 'markup.list') &&
|
||||
!hyphenEmptyListItem(tokens)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function countIndent(line: string): number {
|
||||
return line.match(/\t| {4}/g)?.length || 0;
|
||||
}
|
||||
|
||||
// Finds the list item with indent level `prevIndent`.
|
||||
function findPrevListNum(editor: any, row: number, indent: number) {
|
||||
while (row > 0) {
|
||||
row--;
|
||||
const line = editor.session.getLine(row);
|
||||
|
||||
if (countIndent(line) === indent) {
|
||||
const num = markdownUtils.olLineNumber(line.trimLeft());
|
||||
if (num) {
|
||||
return num;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
interface HookDependencies {
|
||||
editor: any;
|
||||
}
|
||||
|
||||
export default function useListIdent(dependencies: HookDependencies) {
|
||||
const { editor } = dependencies;
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
// Markdown list indentation. (https://github.com/laurent22/joplin/pull/2713)
|
||||
// If the current line starts with `markup.list` token,
|
||||
// hitting `Tab` key indents the line instead of inserting tab at cursor.
|
||||
const originalEditorIndent = editor.indent;
|
||||
|
||||
editor.indent = function() {
|
||||
const range = selectionRange(editor);
|
||||
if (range.isEmpty()) {
|
||||
const row = range.start.row;
|
||||
const tokens = listTokens(this, row);
|
||||
|
||||
if (tokens.length > 0) {
|
||||
if (tokens[0].value.search(/\d+\./) != -1) {
|
||||
const line = this.session.getLine(row);
|
||||
const n = findPrevListNum(this, row, countIndent(line) + 1) + 1;
|
||||
this.session.replace(
|
||||
{
|
||||
start: { row, column: 0 },
|
||||
end: { row, column: tokens[0].value.length },
|
||||
},
|
||||
tokens[0].value.replace(/\d+\./, `${n}.`)
|
||||
);
|
||||
}
|
||||
|
||||
this.session.indentRows(row, row, '\t');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (originalEditorIndent) originalEditorIndent.call(this);
|
||||
};
|
||||
|
||||
// Correct the number of numbered list item when outdenting.
|
||||
editor.commands.addCommand({
|
||||
name: 'markdownOutdent',
|
||||
bindKey: { win: 'Shift+Tab', mac: 'Shift+Tab' },
|
||||
multiSelectAction: 'forEachLine',
|
||||
exec: function(editor: any) {
|
||||
const range = selectionRange(editor);
|
||||
|
||||
if (range.isEmpty()) {
|
||||
const row = range.start.row;
|
||||
|
||||
const tokens = editor.session.getTokens(row);
|
||||
if (tokens.length && tokens[0].type === 'markup.list') {
|
||||
const matches = tokens[0].value.match(/^(\t+)\d+\./);
|
||||
if (matches && matches.length) {
|
||||
const indent = countIndent(matches[1]);
|
||||
const n = findPrevListNum(editor, row, indent - 1) + 1;
|
||||
console.log(n);
|
||||
editor.session.replace(
|
||||
{
|
||||
start: { row, column: 0 },
|
||||
end: { row, column: tokens[0].value.length },
|
||||
},
|
||||
tokens[0].value.replace(/\d+\./, `${n}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.blockOutdent();
|
||||
},
|
||||
readonly: false,
|
||||
});
|
||||
|
||||
// Delete a list markup (e.g. `- `) from an empty list item on hitting Enter.
|
||||
// (https://github.com/laurent22/joplin/pull/2772)
|
||||
editor.commands.addCommand({
|
||||
name: 'markdownEnter',
|
||||
bindKey: 'Enter',
|
||||
multiSelectAction: 'forEach',
|
||||
exec: function(editor: any) {
|
||||
const range = editor.getSelectionRange();
|
||||
const tokens = listTokens(editor, range.start.row);
|
||||
|
||||
const emptyListItem =
|
||||
tokens.length === 1 || hyphenEmptyListItem(tokens);
|
||||
const emptyCheckboxItem =
|
||||
tokens.length === 3 &&
|
||||
['[ ]', '[x]'].includes(tokens[1].value) &&
|
||||
tokens[2].value === ' ';
|
||||
|
||||
if (!range.isEmpty() || !(emptyListItem || emptyCheckboxItem)) {
|
||||
editor.insert('\n');
|
||||
// Cursor can go out of the view after inserting '\n'.
|
||||
editor.renderer.scrollCursorIntoView();
|
||||
return;
|
||||
}
|
||||
|
||||
const row = range.start.row;
|
||||
const line = editor.session.getLine(row);
|
||||
let indent = editor
|
||||
.getSession()
|
||||
.getMode()
|
||||
.getNextLineIndent(null, line);
|
||||
if (indent.startsWith('\t')) {
|
||||
indent = indent.slice(1);
|
||||
} else {
|
||||
indent = '';
|
||||
}
|
||||
|
||||
editor.session.replace(
|
||||
{
|
||||
start: { row, column: 0 },
|
||||
end: { row, column: line.length },
|
||||
},
|
||||
indent
|
||||
);
|
||||
},
|
||||
readOnly: false,
|
||||
});
|
||||
|
||||
return () => {
|
||||
editor.indent = originalEditorIndent;
|
||||
editor.commands.removeCommand('markdownOutdent');
|
||||
editor.commands.removeCommand('markdownEnter');
|
||||
};
|
||||
}, [editor]);
|
||||
}
|
@ -113,7 +113,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
},
|
||||
supportsCommand: (/* name:string*/) => {
|
||||
// TODO: not implemented, currently only used for "search" command
|
||||
// which is not directly supported by Ace Editor.
|
||||
// which is not directly supported by this Editor.
|
||||
return false;
|
||||
},
|
||||
execCommand: async (cmd: EditorCommand) => {
|
||||
@ -479,36 +479,12 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
|
||||
useEffect(() => {
|
||||
if (props.searchMarkers !== previousSearchMarkers || renderedBody !== previousRenderedBody) {
|
||||
// SEARCHHACK
|
||||
// TODO: remove this options hack when aceeditor is removed
|
||||
// Currently the webviewRef will send out an ipcMessage to set the results count
|
||||
// Also setting it here will start an infinite loop of repeating the search
|
||||
// Unfortunately we can't remove the function in the webview setMarkers
|
||||
// until the aceeditor is remove.
|
||||
// The below search is more accurate than the webview based one as it searches
|
||||
// the text and not rendered html (rendered html fails if there is a match
|
||||
// in a katex block)
|
||||
// Once AceEditor is removed the options definition below can be removed and
|
||||
// props.searchMarkers.options can be directly passed to as the 3rd argument below
|
||||
// (replacing options)
|
||||
let options = { notFromAce: true };
|
||||
if (props.searchMarkers.options) {
|
||||
options = Object.assign({}, props.searchMarkers.options, options);
|
||||
}
|
||||
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, options);
|
||||
// SEARCHHACK
|
||||
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
|
||||
if (editorRef.current) {
|
||||
const matches = editorRef.current.setMarkers(props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
|
||||
// SEARCHHACK
|
||||
// TODO: when aceeditor is removed then this check will be performed in the NoteSearchbar
|
||||
// End the if statement can be removed in favor of simply returning matches
|
||||
if (props.visiblePanes.includes('editor')) {
|
||||
props.setLocalSearchResultCount(matches);
|
||||
} else {
|
||||
props.setLocalSearchResultCount(-1);
|
||||
}
|
||||
// end SEARCHHACK
|
||||
props.setLocalSearchResultCount(matches);
|
||||
}
|
||||
}
|
||||
}, [props.searchMarkers, props.setLocalSearchResultCount, renderedBody]);
|
||||
|
@ -267,7 +267,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
} else if (cmd.value.type === 'files') {
|
||||
insertResourcesIntoContentRef.current(cmd.value.paths, { createFileURL: !!cmd.value.createFileURL });
|
||||
} else {
|
||||
reg.logger().warn('AceEditor: unsupported drop item: ', cmd);
|
||||
reg.logger().warn('TinyMCE: unsupported drop item: ', cmd);
|
||||
}
|
||||
} else {
|
||||
commandProcessed = false;
|
||||
|
@ -2,7 +2,6 @@ import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import TinyMCE from './NoteBody/TinyMCE/TinyMCE';
|
||||
import AceEditor from './NoteBody/AceEditor/AceEditor';
|
||||
import CodeMirror from './NoteBody/CodeMirror/CodeMirror';
|
||||
import { connect } from 'react-redux';
|
||||
import MultiNoteActions from '../MultiNoteActions';
|
||||
@ -398,8 +397,6 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
if (props.bodyEditor === 'TinyMCE') {
|
||||
editor = <TinyMCE {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'AceEditor') {
|
||||
editor = <AceEditor {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'CodeMirror') {
|
||||
editor = <CodeMirror {...editorProps}/>;
|
||||
} else {
|
||||
@ -466,6 +463,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
onNext={localSearch_next}
|
||||
onPrevious={localSearch_previous}
|
||||
onClose={localSearch_close}
|
||||
visiblePanes={props.noteVisiblePanes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -148,14 +148,7 @@ class NoteSearchBarComponent extends React.Component {
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// Currently searching in the viewer does not support jumping between matches
|
||||
// So we explicitly disable those commands when only the viewer is open (this is
|
||||
// currently signaled by results count being set to -1, but once Ace editor is removed
|
||||
// we can observe the visible panes directly).
|
||||
// SEARCHHACK
|
||||
// TODO: remove the props.resultCount check here and replace it by checking visible panes directly
|
||||
const allowScrolling = this.props.resultCount !== -1;
|
||||
// end SEARCHHACK
|
||||
const allowScrolling = this.props.visiblePanes.indexOf('editor') >= 0;
|
||||
|
||||
const viewerWarning = (
|
||||
<div style={textStyle}>
|
||||
|
@ -277,25 +277,7 @@
|
||||
let selectedElement = null;
|
||||
let elementIndex = 0;
|
||||
|
||||
const onEachElement = (element) => {
|
||||
// SEARCHHACK
|
||||
// TODO: remove notFromAce hack when removing aceeditor
|
||||
// when removing just remove the 'notFromAce' part and leave the rest alone
|
||||
if (!('selectedIndex' in options) || 'notFromAce' in options) return;
|
||||
// SEARCHHACK
|
||||
|
||||
if (('selectedIndex' in options) && elementIndex === options.selectedIndex) {
|
||||
markSelectedElement_ = element;
|
||||
element.classList.add('mark-selected');
|
||||
selectedElement = element;
|
||||
}
|
||||
|
||||
elementIndex++;
|
||||
}
|
||||
|
||||
const markKeywordOptions = {
|
||||
each: onEachElement,
|
||||
};
|
||||
const markKeywordOptions = {};
|
||||
|
||||
if ('separateWordSearch' in options) markKeywordOptions.separateWordSearch = options.separateWordSearch;
|
||||
|
||||
@ -307,22 +289,6 @@
|
||||
replaceRegexDiacritics: replaceRegexDiacritics,
|
||||
}, markKeywordOptions);
|
||||
}
|
||||
|
||||
// SEARCHHACK
|
||||
// TODO: Remove this block (until the other SEARCHHACK marker) when removing Ace
|
||||
// HACK: Aceeditor uses this view to handle all the searching
|
||||
// The newer editor wont and this needs to be disabled in order to
|
||||
// prevent an infinite loop
|
||||
if (!('notFromAce' in options)) {
|
||||
ipcProxySendToHost('setMarkerCount', elementIndex);
|
||||
|
||||
// We only scroll the element into view if the search just happened. So when the user type the search
|
||||
// or select the next/previous result, we scroll into view. However for other actions that trigger a
|
||||
// re-render, we don't scroll as this is normally not wanted.
|
||||
// This is to go around this issue: https://github.com/laurent22/joplin/issues/1833
|
||||
if (selectedElement && Date.now() - options.searchTimestamp <= 1000) selectedElement.scrollIntoView();
|
||||
}
|
||||
// SEARCHHACK
|
||||
}
|
||||
|
||||
let markLoader_ = { state: 'idle', whenDone: null };
|
||||
|
@ -178,7 +178,6 @@
|
||||
"promise": "^8.0.1",
|
||||
"query-string": "^5.1.1",
|
||||
"react": "^16.9.0",
|
||||
"react-ace": "^6.1.4",
|
||||
"react-datetime": "^2.14.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-redux": "^5.0.7",
|
||||
|
@ -63,13 +63,6 @@ a {
|
||||
transition: 0.3s;
|
||||
opacity: 1;
|
||||
}
|
||||
/* By default, the Ice Editor displays invalid characters, such as non-breaking spaces
|
||||
as red boxes, but since those are actually valid characters and common in imported
|
||||
Evernote data, we hide them here. */
|
||||
.ace-chrome .ace_invisible_space {
|
||||
background-color: transparent !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.note-list .list-item-container:hover {
|
||||
background-color: rgba(0,160,255,0.1) !important;
|
||||
|
@ -392,14 +392,6 @@ class Setting extends BaseModel {
|
||||
appTypes: ['desktop'],
|
||||
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'] },
|
||||
'folders.sortOrder.field': {
|
||||
value: 'title',
|
||||
|
@ -28,7 +28,6 @@ const aritimStyle = {
|
||||
codeBorderColor: '#141a21', // Single line code border, and tables
|
||||
codeColor: '#005b47', // Single line code text
|
||||
|
||||
aceEditorTheme: 'chaos',
|
||||
codeMirrorTheme: 'monokai',
|
||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||
|
||||
|
@ -28,7 +28,6 @@ const darkStyle = {
|
||||
codeBackgroundColor: 'rgb(47, 48, 49)',
|
||||
codeBorderColor: 'rgb(70, 70, 70)',
|
||||
|
||||
aceEditorTheme: 'twilight',
|
||||
codeMirrorTheme: 'material-darker',
|
||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||
|
||||
|
@ -28,7 +28,6 @@ const draculaStyle = {
|
||||
codeBorderColor: '#f8f8f2',
|
||||
codeColor: '#50fa7b',
|
||||
|
||||
aceEditorTheme: 'dracula',
|
||||
codeMirrorTheme: 'dracula',
|
||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||
};
|
||||
|
@ -31,7 +31,6 @@ const lightStyle = {
|
||||
codeBorderColor: 'rgb(220, 220, 220)',
|
||||
codeColor: 'rgb(0,0,0)',
|
||||
|
||||
aceEditorTheme: 'chrome',
|
||||
codeMirrorTheme: 'default',
|
||||
codeThemeCss: 'atom-one-light.css',
|
||||
};
|
||||
|
@ -74,7 +74,6 @@ const nordStyle = {
|
||||
codeBorderColor: nord[2],
|
||||
codeColor: nord[13],
|
||||
|
||||
aceEditorTheme: 'terminal',
|
||||
codeMirrorTheme: 'nord',
|
||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||
};
|
||||
|
@ -28,7 +28,6 @@ const solarizedDarkStyle = {
|
||||
codeBorderColor: '#696969',
|
||||
codeColor: '#fdf6e3',
|
||||
|
||||
aceEditorTheme: 'twilight',
|
||||
codeMirrorTheme: 'solarized dark',
|
||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||
};
|
||||
|
@ -28,7 +28,6 @@ const solarizedLightStyle = {
|
||||
codeBorderColor: '#eee8d5',
|
||||
codeColor: '#002b36',
|
||||
|
||||
aceEditorTheme: 'tomorrow',
|
||||
codeMirrorTheme: 'solarized light',
|
||||
codeThemeCss: 'atom-one-light.css',
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user