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/isCursorAtBeginning.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/setupVim.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/isCursorAtBeginning.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/setupVim.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
|
@ -32,6 +32,7 @@ import handlePasteEvent from './utils/handlePasteEvent';
|
||||
import biDirectionalTextExtension from './utils/biDirectionalTextExtension';
|
||||
import searchExtension from './utils/searchExtension';
|
||||
import isCursorAtBeginning from './utils/isCursorAtBeginning';
|
||||
import overwriteModeExtension from './utils/overwriteModeExtension';
|
||||
|
||||
// Newer versions of CodeMirror by default use Chrome's EditContext API.
|
||||
// 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)
|
||||
decoratorExtension,
|
||||
|
||||
biDirectionalTextExtension,
|
||||
overwriteModeExtension,
|
||||
|
||||
props.localisations ? EditorState.phrases.of(props.localisations) : [],
|
||||
|
||||
|
@ -1,16 +1,26 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
|
||||
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
|
||||
// The important part is the userEvent: input.type.
|
||||
|
||||
const selection = editor.state.selection;
|
||||
editor.dispatch({
|
||||
const defaultTransaction = editor.state.update({
|
||||
changes: [{ from: selection.main.head, insert: text }],
|
||||
selection: { anchor: selection.main.head + text.length },
|
||||
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;
|
||||
|
@ -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