You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-13 00:10:37 +02:00
Desktop: Accessibility: Improve note title focus handling (#10932)
This commit is contained in:
@ -297,6 +297,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
|
|||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHandler.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
|
||||||
@ -844,6 +845,7 @@ packages/editor/CodeMirror/utils/formatting/types.js
|
|||||||
packages/editor/CodeMirror/utils/getSearchState.js
|
packages/editor/CodeMirror/utils/getSearchState.js
|
||||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
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/isInSyntaxNode.js
|
packages/editor/CodeMirror/utils/isInSyntaxNode.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
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -274,6 +274,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
|
|||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHandler.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
|
||||||
@ -821,6 +822,7 @@ packages/editor/CodeMirror/utils/formatting/types.js
|
|||||||
packages/editor/CodeMirror/utils/getSearchState.js
|
packages/editor/CodeMirror/utils/getSearchState.js
|
||||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
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/isInSyntaxNode.js
|
packages/editor/CodeMirror/utils/isInSyntaxNode.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
|
||||||
|
@ -4,14 +4,18 @@ export const declaration: CommandDeclaration = {
|
|||||||
name: 'focusElement',
|
name: 'focusElement',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface FocusElementOptions {
|
||||||
|
moveCursorToStart: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const runtime = (): CommandRuntime => {
|
export const runtime = (): CommandRuntime => {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
execute: async (_context: any, target: string) => {
|
execute: async (_context: any, target: string, options?: FocusElementOptions) => {
|
||||||
if (target === 'noteBody') return CommandService.instance().execute('focusElementNoteBody');
|
if (target === 'noteBody') return CommandService.instance().execute('focusElementNoteBody', options);
|
||||||
if (target === 'noteList') return CommandService.instance().execute('focusElementNoteList');
|
if (target === 'noteList') return CommandService.instance().execute('focusElementNoteList');
|
||||||
if (target === 'sideBar') return CommandService.instance().execute('focusElementSideBar');
|
if (target === 'sideBar') return CommandService.instance().execute('focusElementSideBar');
|
||||||
if (target === 'noteTitle') return CommandService.instance().execute('focusElementNoteTitle');
|
if (target === 'noteTitle') return CommandService.instance().execute('focusElementNoteTitle', options);
|
||||||
throw new Error(`Invalid focus target: ${target}`);
|
throw new Error(`Invalid focus target: ${target}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -27,6 +27,7 @@ import useContextMenu from '../utils/useContextMenu';
|
|||||||
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
||||||
import Toolbar from '../Toolbar';
|
import Toolbar from '../Toolbar';
|
||||||
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
|
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
|
||||||
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
|
|
||||||
const logger = Logger.create('CodeMirror6');
|
const logger = Logger.create('CodeMirror6');
|
||||||
const logDebug = (message: string) => logger.debug(message);
|
const logDebug = (message: string) => logger.debug(message);
|
||||||
@ -354,6 +355,10 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
}
|
}
|
||||||
}, [editor_scroll, codeMirror_change, props.setLocalSearch, props.setShowLocalSearch]);
|
}, [editor_scroll, codeMirror_change, props.setLocalSearch, props.setShowLocalSearch]);
|
||||||
|
|
||||||
|
const onSelectPastBeginning = useCallback(() => {
|
||||||
|
void CommandService.instance().execute('focusElement', 'noteTitle');
|
||||||
|
}, []);
|
||||||
|
|
||||||
const editorSettings = useMemo((): EditorSettings => {
|
const editorSettings = useMemo((): EditorSettings => {
|
||||||
const isHTMLNote = props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML;
|
const isHTMLNote = props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML;
|
||||||
|
|
||||||
@ -403,6 +408,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
onEvent={onEditorEvent}
|
onEvent={onEditorEvent}
|
||||||
onLogMessage={logDebug}
|
onLogMessage={logDebug}
|
||||||
onEditorPaste={onEditorPaste}
|
onEditorPaste={onEditorPaste}
|
||||||
|
onSelectPastBeginning={onSelectPastBeginning}
|
||||||
externalSearch={props.searchMarkers}
|
externalSearch={props.searchMarkers}
|
||||||
useLocalSearch={props.useLocalSearch}
|
useLocalSearch={props.useLocalSearch}
|
||||||
/>
|
/>
|
||||||
|
@ -9,6 +9,7 @@ import Logger from '@joplin/utils/Logger';
|
|||||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||||
import { MarkupLanguage } from '@joplin/renderer';
|
import { MarkupLanguage } from '@joplin/renderer';
|
||||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
|
import { FocusElementOptions } from '../../../../../commands/focusElement';
|
||||||
|
|
||||||
const logger = Logger.create('CodeMirror 6 commands');
|
const logger = Logger.create('CodeMirror 6 commands');
|
||||||
|
|
||||||
@ -103,9 +104,15 @@ const useEditorCommands = (props: Props) => {
|
|||||||
logger.warn('CodeMirror execCommand: unsupported command: ', value.name);
|
logger.warn('CodeMirror execCommand: unsupported command: ', value.name);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'editor.focus': () => {
|
'editor.focus': (options?: FocusElementOptions) => {
|
||||||
if (props.visiblePanes.indexOf('editor') >= 0) {
|
if (props.visiblePanes.indexOf('editor') >= 0) {
|
||||||
focus('useEditorCommands::editor.focus', editorRef.current.editor);
|
focus('useEditorCommands::editor.focus', editorRef.current.editor);
|
||||||
|
if (options?.moveCursorToStart) {
|
||||||
|
editorRef.current.editor.dispatch({
|
||||||
|
selection: { anchor: 0 },
|
||||||
|
scrollIntoView: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// If we just call focus() then the iframe is focused,
|
// If we just call focus() then the iframe is focused,
|
||||||
// but not its content, such that scrolling up / down
|
// but not its content, such that scrolling up / down
|
||||||
|
@ -39,6 +39,7 @@ const { clipboard } = require('electron');
|
|||||||
const supportedLocales = require('./supportedLocales');
|
const supportedLocales = require('./supportedLocales');
|
||||||
import { hasProtocol } from '@joplin/utils/url';
|
import { hasProtocol } from '@joplin/utils/url';
|
||||||
import useTabIndenter from './utils/useTabIndenter';
|
import useTabIndenter from './utils/useTabIndenter';
|
||||||
|
import useKeyboardRefocusHandler from './utils/useKeyboardRefocusHandler';
|
||||||
|
|
||||||
const logger = Logger.create('TinyMCE');
|
const logger = Logger.create('TinyMCE');
|
||||||
|
|
||||||
@ -130,6 +131,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
|||||||
usePluginServiceRegistration(ref);
|
usePluginServiceRegistration(ref);
|
||||||
useContextMenu(editor, props.plugins, props.dispatch, props.htmlToMarkdown, props.markupToHtml);
|
useContextMenu(editor, props.plugins, props.dispatch, props.htmlToMarkdown, props.markupToHtml);
|
||||||
useTabIndenter(editor);
|
useTabIndenter(editor);
|
||||||
|
useKeyboardRefocusHandler(editor);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
const dispatchDidUpdate = (editor: any) => {
|
const dispatchDidUpdate = (editor: any) => {
|
||||||
@ -218,6 +220,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
|||||||
editor.insertContent(result.html);
|
editor.insertContent(result.html);
|
||||||
} else if (cmd.name === 'editor.focus') {
|
} else if (cmd.name === 'editor.focus') {
|
||||||
focus('TinyMCE::editor.focus', editor);
|
focus('TinyMCE::editor.focus', editor);
|
||||||
|
if (cmd.value?.moveCursorToStart) {
|
||||||
|
editor.selection.placeCaretAt(0, 0);
|
||||||
|
editor.selection.setCursorLocation(
|
||||||
|
editor.dom.root,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (cmd.name === 'editor.execCommand') {
|
} else if (cmd.name === 'editor.execCommand') {
|
||||||
if (!('ui' in cmd.value)) cmd.value.ui = false;
|
if (!('ui' in cmd.value)) cmd.value.ui = false;
|
||||||
if (!('value' in cmd.value)) cmd.value.value = null;
|
if (!('value' in cmd.value)) cmd.value.value = null;
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import type { Editor, EditorEvent } from 'tinymce';
|
||||||
|
|
||||||
|
const useKeyboardRefocusHandler = (editor: Editor) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor) return () => {};
|
||||||
|
|
||||||
|
const isAtBeginningOf = (element: Node, parent: HTMLElement) => {
|
||||||
|
if (!parent.contains(element)) return false;
|
||||||
|
|
||||||
|
while (
|
||||||
|
element &&
|
||||||
|
element.parentNode !== parent &&
|
||||||
|
element.parentNode?.firstChild === element
|
||||||
|
) {
|
||||||
|
element = element.parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!element && element.parentNode?.firstChild === element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventHandler = (event: EditorEvent<KeyboardEvent>) => {
|
||||||
|
if (!event.isDefaultPrevented() && event.key === 'ArrowUp') {
|
||||||
|
const selection = editor.selection.getRng();
|
||||||
|
if (selection.startOffset === 0 &&
|
||||||
|
selection.collapsed &&
|
||||||
|
isAtBeginningOf(selection.startContainer, editor.dom.getRoot())
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
void CommandService.instance().execute('focusElement', 'noteTitle');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.on('keydown', eventHandler);
|
||||||
|
return () => {
|
||||||
|
editor.off('keydown', eventHandler);
|
||||||
|
};
|
||||||
|
}, [editor]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useKeyboardRefocusHandler;
|
@ -78,18 +78,13 @@ function styles_(props: Props) {
|
|||||||
export default function NoteTitleBar(props: Props) {
|
export default function NoteTitleBar(props: Props) {
|
||||||
const styles = styles_(props);
|
const styles = styles_(props);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
const onTitleKeydown: React.KeyboardEventHandler<HTMLInputElement> = useCallback((event) => {
|
||||||
const onTitleKeydown = useCallback((event: any) => {
|
const titleElement = event.currentTarget;
|
||||||
const keyCode = event.keyCode;
|
const selectionAtEnd = titleElement.selectionEnd === titleElement.value.length;
|
||||||
|
if ((event.key === 'ArrowDown' && selectionAtEnd) || event.key === 'Enter') {
|
||||||
if (keyCode === 9) { // TAB
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
const moveCursorToStart = event.key === 'ArrowDown';
|
||||||
if (event.shiftKey) {
|
void CommandService.instance().execute('focusElement', 'noteBody', { moveCursorToStart });
|
||||||
void CommandService.instance().execute('focusElement', 'noteList');
|
|
||||||
} else {
|
|
||||||
void CommandService.instance().execute('focusElement', 'noteBody');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
|
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import { FocusElementOptions } from '../../../commands/focusElement';
|
||||||
|
|
||||||
export const declaration: CommandDeclaration = {
|
export const declaration: CommandDeclaration = {
|
||||||
name: 'focusElementNoteBody',
|
name: 'focusElementNoteBody',
|
||||||
@ -10,8 +11,8 @@ export const declaration: CommandDeclaration = {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
export const runtime = (comp: any): CommandRuntime => {
|
export const runtime = (comp: any): CommandRuntime => {
|
||||||
return {
|
return {
|
||||||
execute: async () => {
|
execute: async (_context: unknown, options?: FocusElementOptions) => {
|
||||||
comp.editorRef.current.execCommand({ name: 'editor.focus' });
|
comp.editorRef.current.execCommand({ name: 'editor.focus', value: options });
|
||||||
},
|
},
|
||||||
enabledCondition: 'oneNoteSelected',
|
enabledCondition: 'oneNoteSelected',
|
||||||
};
|
};
|
||||||
|
@ -62,12 +62,19 @@ test.describe('main', () => {
|
|||||||
'',
|
'',
|
||||||
'Sum: $\\sum_{x=0}^{100} \\tan x$',
|
'Sum: $\\sum_{x=0}^{100} \\tan x$',
|
||||||
];
|
];
|
||||||
|
let firstLine = true;
|
||||||
for (const line of noteText) {
|
for (const line of noteText) {
|
||||||
if (line) {
|
if (line) {
|
||||||
await mainWindow.keyboard.press('Shift+Tab');
|
if (!firstLine) {
|
||||||
|
// Remove any auto-indentation, but avoid pressing shift-tab at
|
||||||
|
// the beginning of the editor.
|
||||||
|
await mainWindow.keyboard.press('Shift+Tab');
|
||||||
|
}
|
||||||
|
|
||||||
await mainWindow.keyboard.type(line);
|
await mainWindow.keyboard.type(line);
|
||||||
}
|
}
|
||||||
await mainWindow.keyboard.press('Enter');
|
await mainWindow.keyboard.press('Enter');
|
||||||
|
firstLine = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should render mermaid
|
// Should render mermaid
|
||||||
|
@ -119,5 +119,29 @@ test.describe('richTextEditor', () => {
|
|||||||
await editor.toggleEditorsButton.click();
|
await editor.toggleEditorsButton.click();
|
||||||
await expect(editor.codeMirrorEditor).toHaveText('This is a test. Test! Another: !');
|
await expect(editor.codeMirrorEditor).toHaveText('This is a test. Test! Another: !');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should be possible to navigate between the note title and rich text editor with enter/down/up keys', async ({ mainWindow }) => {
|
||||||
|
const mainScreen = new MainScreen(mainWindow);
|
||||||
|
await mainScreen.createNewNote('Testing keyboard navigation!');
|
||||||
|
|
||||||
|
const editor = mainScreen.noteEditor;
|
||||||
|
await editor.toggleEditorsButton.click();
|
||||||
|
|
||||||
|
await editor.richTextEditor.waitFor();
|
||||||
|
|
||||||
|
await editor.noteTitleInput.click();
|
||||||
|
await expect(editor.noteTitleInput).toBeFocused();
|
||||||
|
|
||||||
|
await mainWindow.keyboard.press('End');
|
||||||
|
await mainWindow.keyboard.press('ArrowDown');
|
||||||
|
await expect(editor.richTextEditor).toBeFocused();
|
||||||
|
|
||||||
|
await mainWindow.keyboard.press('ArrowUp');
|
||||||
|
await expect(editor.noteTitleInput).toBeFocused();
|
||||||
|
|
||||||
|
await mainWindow.keyboard.press('Enter');
|
||||||
|
await expect(editor.noteTitleInput).not.toBeFocused();
|
||||||
|
await expect(editor.richTextEditor).toBeFocused();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ import insertLineAfter from './editorCommands/insertLineAfter';
|
|||||||
import handlePasteEvent from './utils/handlePasteEvent';
|
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';
|
||||||
|
|
||||||
const createEditor = (
|
const createEditor = (
|
||||||
parentElement: HTMLElement, props: EditorProps,
|
parentElement: HTMLElement, props: EditorProps,
|
||||||
@ -176,12 +177,28 @@ const createEditor = (
|
|||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
keyCommand('Tab', insertOrIncreaseIndent, true),
|
keyCommand('Tab', insertOrIncreaseIndent, true),
|
||||||
keyCommand('Shift-Tab', decreaseIndent, true),
|
keyCommand('Shift-Tab', (view) => {
|
||||||
|
// When at the beginning of the editor, allow shift-tab to act
|
||||||
|
// normally.
|
||||||
|
if (isCursorAtBeginning(view.state)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decreaseIndent(view);
|
||||||
|
}, true),
|
||||||
keyCommand('Mod-Enter', (_: EditorView) => {
|
keyCommand('Mod-Enter', (_: EditorView) => {
|
||||||
insertLineAfter(_);
|
insertLineAfter(_);
|
||||||
return true;
|
return true;
|
||||||
}, true),
|
}, true),
|
||||||
|
|
||||||
|
keyCommand('ArrowUp', (view: EditorView) => {
|
||||||
|
if (isCursorAtBeginning(view.state) && props.onSelectPastBeginning) {
|
||||||
|
props.onSelectPastBeginning();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, true),
|
||||||
|
|
||||||
...standardKeymap, ...historyKeymap, ...searchKeymap,
|
...standardKeymap, ...historyKeymap, ...searchKeymap,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
8
packages/editor/CodeMirror/utils/isCursorAtBeginning.ts
Normal file
8
packages/editor/CodeMirror/utils/isCursorAtBeginning.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
|
||||||
|
const isCursorAtBeginning = (state: EditorState) => {
|
||||||
|
const selection = state.selection;
|
||||||
|
return selection.ranges.length === 1 && selection.main.empty && selection.main.anchor === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isCursorAtBeginning;
|
@ -169,6 +169,7 @@ export interface EditorSettings {
|
|||||||
export type LogMessageCallback = (message: string)=> void;
|
export type LogMessageCallback = (message: string)=> void;
|
||||||
export type OnEventCallback = (event: EditorEvent)=> void;
|
export type OnEventCallback = (event: EditorEvent)=> void;
|
||||||
export type PasteFileCallback = (data: File)=> Promise<void>;
|
export type PasteFileCallback = (data: File)=> Promise<void>;
|
||||||
|
type OnScrollPastBeginningCallback = ()=> void;
|
||||||
|
|
||||||
export interface EditorProps {
|
export interface EditorProps {
|
||||||
settings: EditorSettings;
|
settings: EditorSettings;
|
||||||
@ -176,6 +177,7 @@ export interface EditorProps {
|
|||||||
|
|
||||||
// If null, paste and drag-and-drop will not work for resources unless handled elsewhere.
|
// If null, paste and drag-and-drop will not work for resources unless handled elsewhere.
|
||||||
onPasteFile: PasteFileCallback|null;
|
onPasteFile: PasteFileCallback|null;
|
||||||
|
onSelectPastBeginning?: OnScrollPastBeginningCallback;
|
||||||
onEvent: OnEventCallback;
|
onEvent: OnEventCallback;
|
||||||
onLogMessage: LogMessageCallback;
|
onLogMessage: LogMessageCallback;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user