Destkop: Fix selection-related issues

pull/3244/head
Laurent Cozic 2020-05-21 08:17:08 +01:00
parent 49451dba20
commit b83eee751f
3 changed files with 26 additions and 91 deletions

View File

@ -5,7 +5,7 @@ import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHand
import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
import { textOffsetToCursorPosition, useScrollHandler, useRootWidth, usePrevious, lineLeftSpaces, selectionRangeCurrentLine, selectionRangePreviousLine, currentTextOffset, textOffsetSelection, selectedText, useSelectionRange } from './utils';
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';
@ -94,17 +94,11 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
const contentKeyHasChangedRef = useRef(false);
contentKeyHasChangedRef.current = previousContentKey !== props.contentKey;
// The selection range changes all the time, when the caret moves or
// when the selection changes, so it's best not to make it part of the
// state as it would trigger too many unecessary updates.
const selectionRangeRef = useRef(null);
selectionRangeRef.current = useSelectionRange(editor);
const rootWidth = useRootWidth({ rootRef });
const { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll } = useScrollHandler(editor, webviewRef, props.onScroll);
useListIdent({ editor, selectionRangeRef });
useListIdent({ editor });
const aceEditor_change = useCallback((newBody: string) => {
props_onChangeRef.current({ changeId: null, content: newBody });
@ -113,7 +107,7 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
const wrapSelectionWithStrings = useCallback((string1: string, string2 = '', defaultText = '', replacementText: string = null, byLine = false) => {
if (!editor) return;
const selection = textOffsetSelection(selectionRangeRef.current, props.content);
const selection = textOffsetSelection(selectionRange(editor), props.content);
let newBody = props.content;
@ -140,7 +134,7 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
newBody += props.content.substr(selection.end);
const r = selectionRangeRef.current;
const r = selectionRange(editor);
// Because some insertion strings will have newlines, we'll need to account for them
const str1Split = string1.split(/\r?\n/);
@ -180,7 +174,7 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
}
setTimeout(() => {
const range = selectionRangeRef.current;
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);
@ -204,7 +198,7 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
setTimeout(() => {
if (middleText && newRange) {
const range = selectionRangeRef.current;
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);
@ -222,12 +216,12 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
const addListItem = useCallback((string1, string2 = '', defaultText = '', byLine = false) => {
let newLine = '\n';
const range = selectionRangeRef.current;
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]);
}, [wrapSelectionWithStrings, props.content, editor]);
useImperativeHandle(ref, () => {
return {
@ -283,7 +277,7 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
if (url) wrapSelectionWithStrings('[', `](${url})`);
},
textCode: () => {
const selection = textOffsetSelection(selectionRangeRef.current, props.content);
const selection = textOffsetSelection(selectionRange(editor), props.content);
const string = props.content.substr(selection.start, selection.end - selection.start);
// Look for newlines
@ -301,13 +295,14 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
},
insertText: (value: any) => wrapSelectionWithStrings(value),
attachFile: async () => {
const selection = textOffsetSelection(selectionRangeRef.current, props.content);
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: () => {
let bulletNumber = markdownUtils.olLineNumber(selectionRangeCurrentLine(selectionRangeRef.current, props.content));
if (!bulletNumber) bulletNumber = markdownUtils.olLineNumber(selectionRangePreviousLine(selectionRangeRef.current, props.content));
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);
},
@ -346,12 +341,12 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
}, []);
const editorCutText = useCallback(() => {
const text = selectedText(selectionRangeRef.current, props.content);
const text = selectedText(selectionRange(editor), props.content);
if (!text) return;
clipboard.writeText(text);
const s = textOffsetSelection(selectionRangeRef.current, props.content);
const s = textOffsetSelection(selectionRange(editor), props.content);
if (!s || s.start === s.end) return;
const s1 = props.content.substr(0, s.start);
@ -360,7 +355,7 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
aceEditor_change(s1 + s2);
setTimeout(() => {
const range = selectionRangeRef.current;
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);
@ -369,9 +364,9 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
}, [props.content, editor, aceEditor_change]);
const editorCopyText = useCallback(() => {
const text = selectedText(selectionRangeRef.current, props.content);
const text = selectedText(selectionRange(editor), props.content);
clipboard.writeText(text);
}, [props.content]);
}, [props.content, editor]);
const editorPasteText = useCallback(() => {
wrapSelectionWithStrings(clipboard.readText(), '', '', '');
@ -380,7 +375,7 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
const onEditorContextMenu = useCallback(() => {
const menu = new Menu();
const hasSelectedText = !!selectedText(selectionRangeRef.current, props.content);
const hasSelectedText = !!selectedText(selectionRange(editor), props.content);
const clipboardText = clipboard.readText();
menu.append(
@ -419,7 +414,7 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
);
menu.popup(bridge().window());
}, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste]);
}, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste, editor]);
function aceEditor_load(editor: any) {
setEditor(editor);

View File

@ -42,69 +42,9 @@ export function selectedText(selectionRange: any, body: string) {
return body.substr(selection.start, selection.end - selection.start);
}
function selectionRangesEqual(s1:any, s2:any) {
if (s1 === s2) return true;
if (!s1 && !s2) return true;
if (s1 && !s2) return false;
if (!s1 && s2) return false;
if (s1.start.row !== s2.start.row) return false;
if (s1.start.column !== s2.start.column) return false;
if (s1.end.row !== s2.end.row) return false;
if (s1.end.column !== s2.end.column) return false;
return true;
}
export function useSelectionRange(editor: any) {
const [selectionRange, setSelectionRange] = useState(null);
useEffect(() => {
if (!editor) return () => {};
function updateSelection() {
const ranges = editor.getSelection().getAllRanges();
const firstRange = ranges && ranges.length ? ranges[0] : null;
// Ace Editor might sometimes send multiple "changeSelection" events
// with the same selection range, which triggers unecessary updates
// and even infinite rendering loops. So before setting it on the state
// we deep compare the previous and new selection.
// https://github.com/laurent22/joplin/issues/3200
setSelectionRange((prev:any) => {
if (selectionRangesEqual(prev, firstRange)) return prev;
return firstRange;
});
// if (process.platform === 'linux') {
// const textRange = this.textOffsetSelection();
// if (textRange.start != textRange.end) {
// clipboard.writeText(this.state.note.body.slice(
// Math.min(textRange.start, textRange.end),
// Math.max(textRange.end, textRange.start)), 'selection');
// }
// }
}
function onSelectionChange() {
updateSelection();
}
function onFocus() {
updateSelection();
}
editor.getSession().selection.on('changeSelection', onSelectionChange);
editor.on('focus', onFocus);
return () => {
editor.getSession().selection.off('changeSelection', onSelectionChange);
editor.off('focus', onFocus);
};
}, [editor]);
return selectionRange;
export function selectionRange(editor:any) {
const ranges = editor.getSelection().getAllRanges();
return ranges && ranges.length ? ranges[0] : null;
}
export function textOffsetToCursorPosition(offset: number, body: string) {

View File

@ -1,12 +1,12 @@
import { useEffect } from 'react';
import { selectionRange } from './index';
interface HookDependencies {
editor: any,
selectionRangeRef: any,
}
export default function useListIdent(dependencies:HookDependencies) {
const { editor, selectionRangeRef } = dependencies;
const { editor } = dependencies;
useEffect(() => {
if (!editor) return;
@ -17,7 +17,7 @@ export default function useListIdent(dependencies:HookDependencies) {
const originalEditorIndent = editor.indent;
editor.indent = function() {
const range = selectionRangeRef.current;
const range = selectionRange(editor);
if (range.isEmpty()) {
const row = range.start.row;
const tokens = this.session.getTokens(row);