mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Desktop,Mobile,Web: Add support for overwrite mode in the Markdown editor (#11262)
This commit is contained in:
parent
3732a57af3
commit
bed5297829
@ -873,6 +873,8 @@ packages/editor/CodeMirror/utils/growSelectionToNode.js
|
|||||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||||
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
||||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||||
|
packages/editor/CodeMirror/utils/overwriteModeExtension.test.js
|
||||||
|
packages/editor/CodeMirror/utils/overwriteModeExtension.js
|
||||||
packages/editor/CodeMirror/utils/searchExtension.js
|
packages/editor/CodeMirror/utils/searchExtension.js
|
||||||
packages/editor/CodeMirror/utils/setupVim.js
|
packages/editor/CodeMirror/utils/setupVim.js
|
||||||
packages/editor/SelectionFormatting.js
|
packages/editor/SelectionFormatting.js
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -850,6 +850,8 @@ packages/editor/CodeMirror/utils/growSelectionToNode.js
|
|||||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||||
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
||||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||||
|
packages/editor/CodeMirror/utils/overwriteModeExtension.test.js
|
||||||
|
packages/editor/CodeMirror/utils/overwriteModeExtension.js
|
||||||
packages/editor/CodeMirror/utils/searchExtension.js
|
packages/editor/CodeMirror/utils/searchExtension.js
|
||||||
packages/editor/CodeMirror/utils/setupVim.js
|
packages/editor/CodeMirror/utils/setupVim.js
|
||||||
packages/editor/SelectionFormatting.js
|
packages/editor/SelectionFormatting.js
|
||||||
|
@ -32,6 +32,7 @@ import handlePasteEvent from './utils/handlePasteEvent';
|
|||||||
import biDirectionalTextExtension from './utils/biDirectionalTextExtension';
|
import biDirectionalTextExtension from './utils/biDirectionalTextExtension';
|
||||||
import searchExtension from './utils/searchExtension';
|
import searchExtension from './utils/searchExtension';
|
||||||
import isCursorAtBeginning from './utils/isCursorAtBeginning';
|
import isCursorAtBeginning from './utils/isCursorAtBeginning';
|
||||||
|
import overwriteModeExtension from './utils/overwriteModeExtension';
|
||||||
|
|
||||||
// Newer versions of CodeMirror by default use Chrome's EditContext API.
|
// Newer versions of CodeMirror by default use Chrome's EditContext API.
|
||||||
// While this might be stable enough for desktop use, it causes significant
|
// While this might be stable enough for desktop use, it causes significant
|
||||||
@ -269,7 +270,9 @@ const createEditor = (
|
|||||||
|
|
||||||
// Apply styles to entire lines (block-display decorations)
|
// Apply styles to entire lines (block-display decorations)
|
||||||
decoratorExtension,
|
decoratorExtension,
|
||||||
|
|
||||||
biDirectionalTextExtension,
|
biDirectionalTextExtension,
|
||||||
|
overwriteModeExtension,
|
||||||
|
|
||||||
props.localisations ? EditorState.phrases.of(props.localisations) : [],
|
props.localisations ? EditorState.phrases.of(props.localisations) : [],
|
||||||
|
|
||||||
|
@ -1,16 +1,26 @@
|
|||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
const typeText = (editor: EditorView, text: string) => {
|
const typeText = (editor: EditorView, text: string) => {
|
||||||
// How CodeMirror does this in their tests:
|
const selection = editor.state.selection;
|
||||||
|
const inputHandlers = editor.state.facet(EditorView.inputHandler);
|
||||||
|
|
||||||
|
// See how the upstream CodeMirror tests simulate user input:
|
||||||
// https://github.com/codemirror/autocomplete/blob/fb1c899464df4d36528331412cdd316548134cb2/test/webtest-autocomplete.ts#L116
|
// https://github.com/codemirror/autocomplete/blob/fb1c899464df4d36528331412cdd316548134cb2/test/webtest-autocomplete.ts#L116
|
||||||
// The important part is the userEvent: input.type.
|
// The important part is the userEvent: input.type.
|
||||||
|
const defaultTransaction = editor.state.update({
|
||||||
const selection = editor.state.selection;
|
|
||||||
editor.dispatch({
|
|
||||||
changes: [{ from: selection.main.head, insert: text }],
|
changes: [{ from: selection.main.head, insert: text }],
|
||||||
selection: { anchor: selection.main.head + text.length },
|
selection: { anchor: selection.main.head + text.length },
|
||||||
userEvent: 'input.type',
|
userEvent: 'input.type',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Allows code that uses input handlers to be tested
|
||||||
|
for (const handler of inputHandlers) {
|
||||||
|
if (handler(editor, selection.main.from, selection.main.to, text, () => defaultTransaction)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.dispatch(defaultTransaction);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default typeText;
|
export default typeText;
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import { EditorSelection } from '@codemirror/state';
|
||||||
|
import createTestEditor from '../testUtil/createTestEditor';
|
||||||
|
import overwriteModeExtension, { toggleOverwrite } from './overwriteModeExtension';
|
||||||
|
import typeText from '../testUtil/typeText';
|
||||||
|
import pressReleaseKey from '../testUtil/pressReleaseKey';
|
||||||
|
|
||||||
|
const createEditor = async (initialText: string, defaultEnabled = false) => {
|
||||||
|
const editor = await createTestEditor(initialText, EditorSelection.cursor(0), [], [
|
||||||
|
overwriteModeExtension,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (defaultEnabled) {
|
||||||
|
editor.dispatch({ effects: [toggleOverwrite.of(true)] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('overwriteModeExtension', () => {
|
||||||
|
test('should be disabled by default', async () => {
|
||||||
|
const editor = await createEditor('Test!');
|
||||||
|
|
||||||
|
typeText(editor, 'This should be inserted. ');
|
||||||
|
expect(editor.state.doc.toString()).toBe('This should be inserted. Test!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should overwrite characters while typing', async () => {
|
||||||
|
const editor = await createEditor('Test!', true);
|
||||||
|
|
||||||
|
pressReleaseKey(editor, { key: 'A', code: 'KeyA' });
|
||||||
|
typeText(editor, 'New');
|
||||||
|
expect(editor.state.doc.toString()).toBe('Newt!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be toggled by pressing <insert>', async () => {
|
||||||
|
const editor = await createEditor('Test!');
|
||||||
|
|
||||||
|
pressReleaseKey(editor, { key: 'Insert', code: 'Insert' });
|
||||||
|
typeText(editor, 'Exam');
|
||||||
|
expect(editor.state.doc.toString()).toBe('Exam!');
|
||||||
|
|
||||||
|
pressReleaseKey(editor, { key: 'Insert', code: 'Insert' });
|
||||||
|
typeText(editor, 'ple');
|
||||||
|
expect(editor.state.doc.toString()).toBe('Example!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should insert text if the cursor is at the end of a line', async () => {
|
||||||
|
const editor = await createEditor('\nTest', true);
|
||||||
|
typeText(editor, 'Test! This is a test! ');
|
||||||
|
typeText(editor, 't');
|
||||||
|
typeText(editor, 'e');
|
||||||
|
typeText(editor, 's');
|
||||||
|
typeText(editor, 't');
|
||||||
|
|
||||||
|
expect(editor.state.doc.toString()).toBe('Test! This is a test! test\nTest');
|
||||||
|
});
|
||||||
|
});
|
78
packages/editor/CodeMirror/utils/overwriteModeExtension.ts
Normal file
78
packages/editor/CodeMirror/utils/overwriteModeExtension.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { keymap, EditorView } from '@codemirror/view';
|
||||||
|
import { StateField, Facet, StateEffect } from '@codemirror/state';
|
||||||
|
|
||||||
|
const overwriteModeFacet = Facet.define({
|
||||||
|
combine: values => values[0] ?? false,
|
||||||
|
enables: facet => [
|
||||||
|
EditorView.inputHandler.of((
|
||||||
|
view, _from, _to, text, insert,
|
||||||
|
) => {
|
||||||
|
if (view.composing || view.compositionStarted || view.state.readOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view.state.facet(facet) && text) {
|
||||||
|
const originalTransaction = insert();
|
||||||
|
const newState1 = originalTransaction.state;
|
||||||
|
const emptySelections1 = newState1.selection.ranges.filter(
|
||||||
|
range => range.empty,
|
||||||
|
);
|
||||||
|
|
||||||
|
view.dispatch([
|
||||||
|
originalTransaction,
|
||||||
|
newState1.update({
|
||||||
|
changes: emptySelections1.map(range => {
|
||||||
|
const line = newState1.doc.lineAt(range.to);
|
||||||
|
return {
|
||||||
|
from: range.to,
|
||||||
|
to: Math.min(line.to, range.to + text.length),
|
||||||
|
insert: '',
|
||||||
|
};
|
||||||
|
}).filter(change => change.from !== change.to || change.insert),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
EditorView.theme({
|
||||||
|
'&.-overwrite .cm-cursor': {
|
||||||
|
borderLeftWidth: '0.5em',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toggleOverwrite = StateEffect.define<boolean>();
|
||||||
|
const overwriteModeState = StateField.define({
|
||||||
|
create: () => false,
|
||||||
|
update: (oldValue, tr) => {
|
||||||
|
for (const e of tr.effects) {
|
||||||
|
if (e.is(toggleOverwrite)) {
|
||||||
|
return e.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return oldValue;
|
||||||
|
},
|
||||||
|
provide: (field) => [
|
||||||
|
overwriteModeFacet.from(field),
|
||||||
|
EditorView.editorAttributes.from(field, on => ({
|
||||||
|
class: on ? '-overwrite' : '',
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const overwriteModeExtension = [
|
||||||
|
overwriteModeState,
|
||||||
|
keymap.of([{
|
||||||
|
key: 'Insert',
|
||||||
|
run: (view) => {
|
||||||
|
view.dispatch({
|
||||||
|
effects: toggleOverwrite.of(!view.state.field(overwriteModeState)),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
}]),
|
||||||
|
];
|
||||||
|
|
||||||
|
export default overwriteModeExtension;
|
Loading…
Reference in New Issue
Block a user