You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-23 22:36:32 +02:00
Desktop: Accessibility: Allow toggling between tab navigation and indentation (#11717)
This commit is contained in:
@@ -170,6 +170,7 @@ packages/app-desktop/commands/switchProfile2.js
|
|||||||
packages/app-desktop/commands/switchProfile3.js
|
packages/app-desktop/commands/switchProfile3.js
|
||||||
packages/app-desktop/commands/toggleExternalEditing.js
|
packages/app-desktop/commands/toggleExternalEditing.js
|
||||||
packages/app-desktop/commands/toggleSafeMode.js
|
packages/app-desktop/commands/toggleSafeMode.js
|
||||||
|
packages/app-desktop/commands/toggleTabMovesFocus.js
|
||||||
packages/app-desktop/gui/Button/Button.js
|
packages/app-desktop/gui/Button/Button.js
|
||||||
packages/app-desktop/gui/ClipperConfigScreen.js
|
packages/app-desktop/gui/ClipperConfigScreen.js
|
||||||
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
||||||
@@ -263,6 +264,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
|
|||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteEditor.js
|
packages/app-desktop/gui/NoteEditor/NoteEditor.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
|
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/StatusBar.js
|
||||||
packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
|
packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
|
||||||
packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js
|
packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js
|
||||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
|
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||||
@@ -502,6 +504,7 @@ packages/app-desktop/integration-tests/goToAnything.spec.js
|
|||||||
packages/app-desktop/integration-tests/main.spec.js
|
packages/app-desktop/integration-tests/main.spec.js
|
||||||
packages/app-desktop/integration-tests/markdownEditor.spec.js
|
packages/app-desktop/integration-tests/markdownEditor.spec.js
|
||||||
packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.js
|
packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.js
|
||||||
|
packages/app-desktop/integration-tests/models/EditorCodeDialog.js
|
||||||
packages/app-desktop/integration-tests/models/GoToAnything.js
|
packages/app-desktop/integration-tests/models/GoToAnything.js
|
||||||
packages/app-desktop/integration-tests/models/MainScreen.js
|
packages/app-desktop/integration-tests/models/MainScreen.js
|
||||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -145,6 +145,7 @@ packages/app-desktop/commands/switchProfile2.js
|
|||||||
packages/app-desktop/commands/switchProfile3.js
|
packages/app-desktop/commands/switchProfile3.js
|
||||||
packages/app-desktop/commands/toggleExternalEditing.js
|
packages/app-desktop/commands/toggleExternalEditing.js
|
||||||
packages/app-desktop/commands/toggleSafeMode.js
|
packages/app-desktop/commands/toggleSafeMode.js
|
||||||
|
packages/app-desktop/commands/toggleTabMovesFocus.js
|
||||||
packages/app-desktop/gui/Button/Button.js
|
packages/app-desktop/gui/Button/Button.js
|
||||||
packages/app-desktop/gui/ClipperConfigScreen.js
|
packages/app-desktop/gui/ClipperConfigScreen.js
|
||||||
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
||||||
@@ -238,6 +239,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
|
|||||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteEditor.js
|
packages/app-desktop/gui/NoteEditor/NoteEditor.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
|
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/StatusBar.js
|
||||||
packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
|
packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
|
||||||
packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js
|
packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js
|
||||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
|
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||||
@@ -477,6 +479,7 @@ packages/app-desktop/integration-tests/goToAnything.spec.js
|
|||||||
packages/app-desktop/integration-tests/main.spec.js
|
packages/app-desktop/integration-tests/main.spec.js
|
||||||
packages/app-desktop/integration-tests/markdownEditor.spec.js
|
packages/app-desktop/integration-tests/markdownEditor.spec.js
|
||||||
packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.js
|
packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.js
|
||||||
|
packages/app-desktop/integration-tests/models/EditorCodeDialog.js
|
||||||
packages/app-desktop/integration-tests/models/GoToAnything.js
|
packages/app-desktop/integration-tests/models/GoToAnything.js
|
||||||
packages/app-desktop/integration-tests/models/MainScreen.js
|
packages/app-desktop/integration-tests/models/MainScreen.js
|
||||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import * as switchProfile2 from './switchProfile2';
|
|||||||
import * as switchProfile3 from './switchProfile3';
|
import * as switchProfile3 from './switchProfile3';
|
||||||
import * as toggleExternalEditing from './toggleExternalEditing';
|
import * as toggleExternalEditing from './toggleExternalEditing';
|
||||||
import * as toggleSafeMode from './toggleSafeMode';
|
import * as toggleSafeMode from './toggleSafeMode';
|
||||||
|
import * as toggleTabMovesFocus from './toggleTabMovesFocus';
|
||||||
|
|
||||||
const index: any[] = [
|
const index: any[] = [
|
||||||
copyDevCommand,
|
copyDevCommand,
|
||||||
@@ -39,6 +40,7 @@ const index: any[] = [
|
|||||||
switchProfile3,
|
switchProfile3,
|
||||||
toggleExternalEditing,
|
toggleExternalEditing,
|
||||||
toggleSafeMode,
|
toggleSafeMode,
|
||||||
|
toggleTabMovesFocus,
|
||||||
];
|
];
|
||||||
|
|
||||||
export default index;
|
export default index;
|
||||||
|
|||||||
20
packages/app-desktop/commands/toggleTabMovesFocus.ts
Normal file
20
packages/app-desktop/commands/toggleTabMovesFocus.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import { DesktopCommandContext } from '../services/commands/types';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'toggleTabMovesFocus',
|
||||||
|
label: () => _('Toggle editor tab key navigation'),
|
||||||
|
iconName: 'fas fa-keyboard',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (_context: DesktopCommandContext, enabled: boolean = null) => {
|
||||||
|
const newValue = enabled ?? !Setting.value('editor.tabMovesFocus');
|
||||||
|
Setting.setValue('editor.tabMovesFocus', newValue);
|
||||||
|
},
|
||||||
|
enabledCondition: 'oneNoteSelected',
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -165,6 +165,7 @@ interface Props {
|
|||||||
showNoteCounts: boolean;
|
showNoteCounts: boolean;
|
||||||
uncompletedTodosOnTop: boolean;
|
uncompletedTodosOnTop: boolean;
|
||||||
showCompletedTodos: boolean;
|
showCompletedTodos: boolean;
|
||||||
|
tabMovesFocus: boolean;
|
||||||
// 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
|
||||||
pluginMenuItems: any[];
|
pluginMenuItems: any[];
|
||||||
// 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
|
||||||
@@ -256,6 +257,7 @@ function useMenuStates(menu: any, props: Props) {
|
|||||||
menuItemSetChecked('showNoteCounts', props.showNoteCounts);
|
menuItemSetChecked('showNoteCounts', props.showNoteCounts);
|
||||||
menuItemSetChecked('uncompletedTodosOnTop', props.uncompletedTodosOnTop);
|
menuItemSetChecked('uncompletedTodosOnTop', props.uncompletedTodosOnTop);
|
||||||
menuItemSetChecked('showCompletedTodos', props.showCompletedTodos);
|
menuItemSetChecked('showCompletedTodos', props.showCompletedTodos);
|
||||||
|
menuItemSetChecked('toggleTabMovesFocus', props.tabMovesFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
timeoutId = setTimeout(scheduleUpdate, 150);
|
timeoutId = setTimeout(scheduleUpdate, 150);
|
||||||
@@ -276,6 +278,7 @@ function useMenuStates(menu: any, props: Props) {
|
|||||||
props['notes.sortOrder.reverse'],
|
props['notes.sortOrder.reverse'],
|
||||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||||
props['folders.sortOrder.reverse'],
|
props['folders.sortOrder.reverse'],
|
||||||
|
props.tabMovesFocus,
|
||||||
props.noteListRendererId,
|
props.noteListRendererId,
|
||||||
props.showNoteCounts,
|
props.showNoteCounts,
|
||||||
props.uncompletedTodosOnTop,
|
props.uncompletedTodosOnTop,
|
||||||
@@ -824,6 +827,12 @@ function useMenu(props: Props) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
separator(),
|
separator(),
|
||||||
|
{
|
||||||
|
...menuItemDic['toggleTabMovesFocus'],
|
||||||
|
label: Setting.settingMetadata('editor.tabMovesFocus').label(),
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
separator(),
|
||||||
{
|
{
|
||||||
label: _('Actual Size'),
|
label: _('Actual Size'),
|
||||||
click: () => {
|
click: () => {
|
||||||
@@ -1145,6 +1154,7 @@ const mapStateToProps = (state: AppState): Partial<Props> => {
|
|||||||
['folders.sortOrder.field']: state.settings['folders.sortOrder.field'],
|
['folders.sortOrder.field']: state.settings['folders.sortOrder.field'],
|
||||||
['notes.sortOrder.reverse']: state.settings['notes.sortOrder.reverse'],
|
['notes.sortOrder.reverse']: state.settings['notes.sortOrder.reverse'],
|
||||||
['folders.sortOrder.reverse']: state.settings['folders.sortOrder.reverse'],
|
['folders.sortOrder.reverse']: state.settings['folders.sortOrder.reverse'],
|
||||||
|
tabMovesFocus: state.settings['editor.tabMovesFocus'],
|
||||||
pluginSettings: state.settings['plugins.states'],
|
pluginSettings: state.settings['plugins.states'],
|
||||||
showNoteCounts: state.settings.showNoteCounts,
|
showNoteCounts: state.settings.showNoteCounts,
|
||||||
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
||||||
|
|||||||
@@ -372,10 +372,12 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),
|
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),
|
||||||
keymap: keyboardMode,
|
keymap: keyboardMode,
|
||||||
indentWithTabs: true,
|
indentWithTabs: true,
|
||||||
|
tabMovesFocus: props.tabMovesFocus,
|
||||||
editorLabel: _('Markdown editor'),
|
editorLabel: _('Markdown editor'),
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
props.contentMarkupLanguage, props.disabled, props.keyboardMode, styles.globalTheme,
|
props.contentMarkupLanguage, props.disabled, props.keyboardMode, styles.globalTheme,
|
||||||
|
props.tabMovesFocus,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Update the editor's value
|
// Update the editor's value
|
||||||
|
|||||||
@@ -132,7 +132,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, !props.tabMovesFocus);
|
||||||
useKeyboardRefocusHandler(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
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import { MarkupToHtml } from '@joplin/renderer';
|
import { MarkupToHtml } from '@joplin/renderer';
|
||||||
import { TinyMceEditorEvents } from './types';
|
import { TinyMceEditorEvents } from './types';
|
||||||
|
import { Editor } from 'tinymce';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
const taboverride = require('taboverride');
|
const taboverride = require('taboverride');
|
||||||
|
|
||||||
@@ -13,27 +15,71 @@ interface SourceInfo {
|
|||||||
language: string;
|
language: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
const createTextAreaKeyListeners = () => {
|
||||||
function dialogTextArea_keyDown(event: any) {
|
let hasListeners = true;
|
||||||
|
|
||||||
|
// Selectively enable/disable taboverride based on settings -- remove taboverride
|
||||||
|
// when pressing tab if tab is expected to move focus.
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Tab') {
|
if (event.key === 'Tab') {
|
||||||
window.requestAnimationFrame(() => focus('openEditDialog::dialogTextArea_keyDown', event.target));
|
if (Setting.value('editor.tabMovesFocus')) {
|
||||||
|
taboverride.utils.removeListeners(event.currentTarget);
|
||||||
|
hasListeners = false;
|
||||||
|
} else {
|
||||||
|
// Prevent the default focus-changing behavior
|
||||||
|
event.preventDefault();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
focus('openEditDialog::dialogTextArea_keyDown', event.target);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyUp = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Tab' && !hasListeners) {
|
||||||
|
taboverride.utils.addListeners(event.currentTarget);
|
||||||
|
hasListeners = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { onKeyDown, onKeyUp };
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TextAreaTabHandler {
|
||||||
|
remove(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allows pressing tab in a textarea to input an actual tab (instead of changing focus)
|
// Allows pressing tab in a textarea to input an actual tab (instead of changing focus)
|
||||||
// taboverride will take care of actually inserting the tab character, while the keydown
|
// taboverride will take care of actually inserting the tab character, while the keydown
|
||||||
// event listener will override the default behaviour, which is to focus the next field.
|
// event listener will override the default behaviour, which is to focus the next field.
|
||||||
function enableTextAreaTab(enable: boolean) {
|
function enableTextAreaTab(document: Document): TextAreaTabHandler {
|
||||||
const textAreas = document.getElementsByClassName('tox-textarea');
|
type RemoveCallback = ()=> void;
|
||||||
for (const textArea of textAreas) {
|
const removeCallbacks: RemoveCallback[] = [];
|
||||||
taboverride.set(textArea, enable);
|
|
||||||
|
|
||||||
if (enable) {
|
const textAreas = document.querySelectorAll<HTMLTextAreaElement>('.tox-textarea');
|
||||||
textArea.addEventListener('keydown', dialogTextArea_keyDown);
|
for (const textArea of textAreas) {
|
||||||
} else {
|
const { onKeyDown, onKeyUp } = createTextAreaKeyListeners();
|
||||||
textArea.removeEventListener('keydown', dialogTextArea_keyDown);
|
textArea.addEventListener('keydown', onKeyDown);
|
||||||
|
textArea.addEventListener('keyup', onKeyUp);
|
||||||
|
|
||||||
|
// Enable/disable taboverride **after** the listeners above.
|
||||||
|
// The custom keyup/keydown need to have higher precedence.
|
||||||
|
taboverride.set(textArea, true);
|
||||||
|
|
||||||
|
removeCallbacks.push(() => {
|
||||||
|
taboverride.set(textArea, false);
|
||||||
|
textArea.removeEventListener('keyup', onKeyUp);
|
||||||
|
textArea.removeEventListener('keydown', onKeyDown);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
remove: () => {
|
||||||
|
for (const callback of removeCallbacks) {
|
||||||
|
callback();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -82,9 +128,12 @@ function editableInnerHtml(html: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
||||||
export default function openEditDialog(editor: any, markupToHtml: any, dispatchDidUpdate: Function, editable: any) {
|
export default function openEditDialog(editor: Editor, markupToHtml: any, dispatchDidUpdate: Function, editable: any) {
|
||||||
const source = editable ? findBlockSource(editable) : newBlockSource();
|
const source = editable ? findBlockSource(editable) : newBlockSource();
|
||||||
|
|
||||||
|
const containerDocument = editor.getContainer().ownerDocument;
|
||||||
|
let tabHandler: TextAreaTabHandler|null = null;
|
||||||
|
|
||||||
editor.windowManager.open({
|
editor.windowManager.open({
|
||||||
title: _('Edit'),
|
title: _('Edit'),
|
||||||
size: 'large',
|
size: 'large',
|
||||||
@@ -113,7 +162,7 @@ export default function openEditDialog(editor: any, markupToHtml: any, dispatchD
|
|||||||
dispatchDidUpdate(editor);
|
dispatchDidUpdate(editor);
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
enableTextAreaTab(false);
|
tabHandler?.remove();
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
type: 'panel',
|
type: 'panel',
|
||||||
@@ -124,12 +173,11 @@ export default function openEditDialog(editor: any, markupToHtml: any, dispatchD
|
|||||||
label: 'Language',
|
label: 'Language',
|
||||||
// Katex is a special case with special opening/closing tags
|
// Katex is a special case with special opening/closing tags
|
||||||
// and we don't currently handle switching the language in this case.
|
// and we don't currently handle switching the language in this case.
|
||||||
disabled: source.language === 'katex',
|
enabled: source.language !== 'katex',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
name: 'codeTextArea',
|
name: 'codeTextArea',
|
||||||
value: source.content,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -142,6 +190,6 @@ export default function openEditDialog(editor: any, markupToHtml: any, dispatchD
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
enableTextAreaTab(true);
|
tabHandler = enableTextAreaTab(containerDocument);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import type { Editor, EditorEvent } from 'tinymce';
|
import type { Editor, EditorEvent } from 'tinymce';
|
||||||
|
|
||||||
const useTabIndenter = (editor: Editor) => {
|
const useTabIndenter = (editor: Editor, enabled: boolean) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return () => {};
|
if (!editor || !enabled) return () => {};
|
||||||
|
|
||||||
const canChangeIndentation = () => {
|
const canChangeIndentation = () => {
|
||||||
const selectionElement = editor.selection.getNode();
|
const selectionElement = editor.selection.getNode();
|
||||||
@@ -70,7 +70,7 @@ const useTabIndenter = (editor: Editor) => {
|
|||||||
return () => {
|
return () => {
|
||||||
editor.off('keydown', eventHandler);
|
editor.off('keydown', eventHandler);
|
||||||
};
|
};
|
||||||
}, [editor]);
|
}, [editor, enabled]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useTabIndenter;
|
export default useTabIndenter;
|
||||||
|
|||||||
@@ -16,13 +16,11 @@ import useFolder from './utils/useFolder';
|
|||||||
import styles_ from './styles';
|
import styles_ from './styles';
|
||||||
import { NoteEditorProps, FormNote, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions, NoteBodyEditorRef } from './utils/types';
|
import { NoteEditorProps, FormNote, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions, NoteBodyEditorRef } from './utils/types';
|
||||||
import CommandService from '@joplin/lib/services/CommandService';
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
import ToolbarButton from '../ToolbarButton/ToolbarButton';
|
|
||||||
import Button, { ButtonLevel } from '../Button/Button';
|
import Button, { ButtonLevel } from '../Button/Button';
|
||||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||||
import { _, _n } from '@joplin/lib/locale';
|
import { _, _n } from '@joplin/lib/locale';
|
||||||
import TagList from '../TagList';
|
|
||||||
import NoteTitleBar from './NoteTitle/NoteTitleBar';
|
import NoteTitleBar from './NoteTitle/NoteTitleBar';
|
||||||
import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils';
|
import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
@@ -59,6 +57,7 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
|
|||||||
import WebviewController from '@joplin/lib/services/plugins/WebviewController';
|
import WebviewController from '@joplin/lib/services/plugins/WebviewController';
|
||||||
import AsyncActionQueue, { IntervalType } from '@joplin/lib/AsyncActionQueue';
|
import AsyncActionQueue, { IntervalType } from '@joplin/lib/AsyncActionQueue';
|
||||||
import useResourceUnwatcher from './utils/useResourceUnwatcher';
|
import useResourceUnwatcher from './utils/useResourceUnwatcher';
|
||||||
|
import StatusBar from './StatusBar';
|
||||||
|
|
||||||
const debounce = require('debounce');
|
const debounce = require('debounce');
|
||||||
|
|
||||||
@@ -440,24 +439,6 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
return <div style={emptyDivStyle} ref={containerRef}></div>;
|
return <div style={emptyDivStyle} ref={containerRef}></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTagButton() {
|
|
||||||
return <ToolbarButton
|
|
||||||
themeId={props.themeId}
|
|
||||||
toolbarButtonInfo={props.setTagsToolbarButtonInfo}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTagBar() {
|
|
||||||
const theme = themeStyle(props.themeId);
|
|
||||||
const noteIds = [formNote.id];
|
|
||||||
const instructions = <span onClick={() => { void CommandService.instance().execute('setTags', noteIds); }} style={{ ...theme.clickableTextStyle, whiteSpace: 'nowrap' }}>{_('Click to add tags...')}</span>;
|
|
||||||
const tagList = props.selectedNoteTags.length ? <TagList items={props.selectedNoteTags} /> : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ paddingLeft: 8, display: 'flex', flexDirection: 'row', alignItems: 'center' }}>{tagList}{instructions}</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords);
|
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords);
|
||||||
|
|
||||||
const editorProps: NoteBodyEditorProps = {
|
const editorProps: NoteBodyEditorProps = {
|
||||||
@@ -488,6 +469,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
searchMarkers: searchMarkers,
|
searchMarkers: searchMarkers,
|
||||||
visiblePanes: props.noteVisiblePanes || ['editor', 'viewer'],
|
visiblePanes: props.noteVisiblePanes || ['editor', 'viewer'],
|
||||||
keyboardMode: Setting.value('editor.keyboardMode'),
|
keyboardMode: Setting.value('editor.keyboardMode'),
|
||||||
|
tabMovesFocus: props.tabMovesFocus,
|
||||||
locale: Setting.value('locale'),
|
locale: Setting.value('locale'),
|
||||||
onDrop: onDrop,
|
onDrop: onDrop,
|
||||||
noteToolbarButtonInfos: props.toolbarButtonInfos,
|
noteToolbarButtonInfos: props.toolbarButtonInfos,
|
||||||
@@ -690,10 +672,11 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||||
{renderSearchBar()}
|
{renderSearchBar()}
|
||||||
</div>
|
</div>
|
||||||
<div className="tag-bar" style={{ paddingLeft: theme.editorPaddingLeft, display: 'flex', flexDirection: 'row', alignItems: 'center', height: 40 }}>
|
<StatusBar
|
||||||
{renderTagButton()}
|
noteId={formNote.id}
|
||||||
{renderTagBar()}
|
setTagsToolbarButtonInfo={props.setTagsToolbarButtonInfo}
|
||||||
</div>
|
selectedNoteTags={props.selectedNoteTags}
|
||||||
|
/>
|
||||||
<WarningBanner bodyEditor={props.bodyEditor}/>
|
<WarningBanner bodyEditor={props.bodyEditor}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -750,6 +733,7 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
|||||||
], whenClauseContext)[0] as ToolbarButtonInfo,
|
], whenClauseContext)[0] as ToolbarButtonInfo,
|
||||||
contentMaxWidth: state.settings['style.editor.contentMaxWidth'],
|
contentMaxWidth: state.settings['style.editor.contentMaxWidth'],
|
||||||
scrollbarSize: state.settings['style.scrollbarSize'],
|
scrollbarSize: state.settings['style.scrollbarSize'],
|
||||||
|
tabMovesFocus: state.settings['editor.tabMovesFocus'],
|
||||||
isSafeMode: state.settings.isSafeMode,
|
isSafeMode: state.settings.isSafeMode,
|
||||||
useCustomPdfViewer: false,
|
useCustomPdfViewer: false,
|
||||||
syncUserId: state.settings['sync.userId'],
|
syncUserId: state.settings['sync.userId'],
|
||||||
|
|||||||
88
packages/app-desktop/gui/NoteEditor/StatusBar.tsx
Normal file
88
packages/app-desktop/gui/NoteEditor/StatusBar.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import ToolbarButton from '../ToolbarButton/ToolbarButton';
|
||||||
|
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||||
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { TagEntity } from '@joplin/lib/services/database/types';
|
||||||
|
import TagList from '../TagList';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import KeymapService from '@joplin/lib/services/KeymapService';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
tabMovesFocus: boolean;
|
||||||
|
noteId: string;
|
||||||
|
setTagsToolbarButtonInfo: ToolbarButtonInfo;
|
||||||
|
selectedNoteTags: TagEntity[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusIndicatorProps {
|
||||||
|
commandName: string;
|
||||||
|
showWhenUnfocused: boolean;
|
||||||
|
// Even if not visible, [label] should reflect the current state
|
||||||
|
// of the indicator.
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusIndicator: React.FC<StatusIndicatorProps> = props => {
|
||||||
|
const runCommand = useCallback(() => {
|
||||||
|
void CommandService.instance().execute(props.commandName);
|
||||||
|
}, [props.commandName]);
|
||||||
|
|
||||||
|
const keyshortcuts = KeymapService.instance().getAriaKeyShortcuts(props.commandName);
|
||||||
|
return <span
|
||||||
|
className={`status editor-status-indicator ${props.showWhenUnfocused ? '-show' : ''}`}
|
||||||
|
aria-live='polite'
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className='button'
|
||||||
|
aria-keyshortcuts={keyshortcuts}
|
||||||
|
onClick={runCommand}
|
||||||
|
>
|
||||||
|
{props.label}
|
||||||
|
</button>
|
||||||
|
</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusBar: React.FC<Props> = props => {
|
||||||
|
function renderTagButton() {
|
||||||
|
return <ToolbarButton
|
||||||
|
themeId={props.themeId}
|
||||||
|
toolbarButtonInfo={props.setTagsToolbarButtonInfo}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTagBar() {
|
||||||
|
const theme = themeStyle(props.themeId);
|
||||||
|
const noteIds = [props.noteId];
|
||||||
|
const instructions = <span onClick={() => { void CommandService.instance().execute('setTags', noteIds); }} style={{ ...theme.clickableTextStyle, whiteSpace: 'nowrap' }}>{_('Click to add tags...')}</span>;
|
||||||
|
const tagList = props.selectedNoteTags.length ? <TagList items={props.selectedNoteTags} /> : null;
|
||||||
|
|
||||||
|
return <div className='tag-bar'>
|
||||||
|
{renderTagButton()}
|
||||||
|
<div className='content'>{tagList}{instructions}</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyboardStatus = <StatusIndicator
|
||||||
|
commandName='toggleTabMovesFocus'
|
||||||
|
label={props.tabMovesFocus ? _('Tab moves focus') : _('Tab indents')}
|
||||||
|
showWhenUnfocused={props.tabMovesFocus}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
return <div className='editor-status-bar'>
|
||||||
|
{renderTagBar()}
|
||||||
|
<div className='spacer'/>
|
||||||
|
{keyboardStatus}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect((state: AppState) => {
|
||||||
|
return {
|
||||||
|
themeId: state.settings.theme,
|
||||||
|
tabMovesFocus: state.settings['editor.tabMovesFocus'],
|
||||||
|
};
|
||||||
|
})(StatusBar);
|
||||||
@@ -7,3 +7,6 @@
|
|||||||
@use "./styles/note-editor-viewer-row.scss";
|
@use "./styles/note-editor-viewer-row.scss";
|
||||||
@use "./styles/revision-viewer-root.scss";
|
@use "./styles/revision-viewer-root.scss";
|
||||||
@use "./styles/revision-viewer-title.scss";
|
@use "./styles/revision-viewer-title.scss";
|
||||||
|
@use "./styles/tag-bar.scss";
|
||||||
|
@use "./styles/editor-status-bar.scss";
|
||||||
|
@use "./styles/editor-status-indicator.scss";
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
.editor-status-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
> .spacer {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .status {
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
.editor-status-indicator {
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:has(> :focus-visible), &.-show {
|
||||||
|
width: unset;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
font-size: var(--joplin-font-size-small);
|
||||||
|
background-color: var(--joplin-background-color-active3);
|
||||||
|
color: var(--joplin-color);
|
||||||
|
border: none;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/app-desktop/gui/NoteEditor/styles/tag-bar.scss
Normal file
14
packages/app-desktop/gui/NoteEditor/styles/tag-bar.scss
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.tag-bar {
|
||||||
|
padding-left: var(--joplin-editor-padding-left);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
> .content {
|
||||||
|
padding-left: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ export interface NoteEditorProps {
|
|||||||
watchedResources: any;
|
watchedResources: any;
|
||||||
// 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
|
||||||
highlightedWords: any[];
|
highlightedWords: any[];
|
||||||
|
tabMovesFocus: boolean;
|
||||||
plugins: PluginStates;
|
plugins: PluginStates;
|
||||||
toolbarButtonInfos: ToolbarItem[];
|
toolbarButtonInfos: ToolbarItem[];
|
||||||
setTagsToolbarButtonInfo: ToolbarButtonInfo;
|
setTagsToolbarButtonInfo: ToolbarButtonInfo;
|
||||||
@@ -121,6 +122,7 @@ export interface NoteBodyEditorProps {
|
|||||||
searchMarkers: SearchMarkers;
|
searchMarkers: SearchMarkers;
|
||||||
visiblePanes: string[];
|
visiblePanes: string[];
|
||||||
keyboardMode: string;
|
keyboardMode: string;
|
||||||
|
tabMovesFocus: boolean;
|
||||||
resourceInfos: ResourceInfos;
|
resourceInfos: ResourceInfos;
|
||||||
resourceDirectory: string;
|
resourceDirectory: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export default function() {
|
|||||||
'togglePerFolderSortOrder',
|
'togglePerFolderSortOrder',
|
||||||
'toggleSideBar',
|
'toggleSideBar',
|
||||||
'toggleVisiblePanes',
|
'toggleVisiblePanes',
|
||||||
|
'toggleTabMovesFocus',
|
||||||
'editor.deleteLine',
|
'editor.deleteLine',
|
||||||
'editor.duplicateLine',
|
'editor.duplicateLine',
|
||||||
// We cannot put the undo/redo commands in the menu because they are
|
// We cannot put the undo/redo commands in the menu because they are
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Locator, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
export default class EditorCodeDialog {
|
||||||
|
private readonly dialog: Locator;
|
||||||
|
public readonly textArea: Locator;
|
||||||
|
|
||||||
|
public constructor(page: Page) {
|
||||||
|
this.dialog = page.getByRole('dialog', { name: 'Edit' });
|
||||||
|
this.textArea = this.dialog.locator('textarea');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async waitFor() {
|
||||||
|
await this.dialog.waitFor();
|
||||||
|
await this.textArea.waitFor();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
|
import { ElectronApplication, Locator, Page } from '@playwright/test';
|
||||||
import { Locator, Page } from '@playwright/test';
|
|
||||||
import { expect } from '../util/test';
|
import { expect } from '../util/test';
|
||||||
|
import activateMainMenuItem from '../util/activateMainMenuItem';
|
||||||
|
import EditorCodeDialog from './EditorCodeDialog';
|
||||||
|
|
||||||
export default class NoteEditorPage {
|
export default class NoteEditorPage {
|
||||||
public readonly codeMirrorEditor: Locator;
|
public readonly codeMirrorEditor: Locator;
|
||||||
public readonly noteViewerContainer: Locator;
|
public readonly noteViewerContainer: Locator;
|
||||||
public readonly richTextEditor: Locator;
|
public readonly richTextEditor: Locator;
|
||||||
public readonly noteTitleInput: Locator;
|
public readonly noteTitleInput: Locator;
|
||||||
|
|
||||||
|
public readonly richTextCodeEditor: EditorCodeDialog;
|
||||||
|
|
||||||
public readonly attachFileButton: Locator;
|
public readonly attachFileButton: Locator;
|
||||||
|
public readonly toggleCodeBlockButton: Locator;
|
||||||
public readonly toggleEditorsButton: Locator;
|
public readonly toggleEditorsButton: Locator;
|
||||||
public readonly toggleEditorLayoutButton: Locator;
|
public readonly toggleEditorLayoutButton: Locator;
|
||||||
|
private readonly disableTabNavigationButton: Locator;
|
||||||
|
|
||||||
public readonly editorSearchInput: Locator;
|
public readonly editorSearchInput: Locator;
|
||||||
public readonly viewerSearchInput: Locator;
|
public readonly viewerSearchInput: Locator;
|
||||||
|
|
||||||
private readonly containerLocator: Locator;
|
private readonly containerLocator: Locator;
|
||||||
|
|
||||||
public constructor(page: Page) {
|
public constructor(page: Page) {
|
||||||
@@ -20,12 +28,16 @@ export default class NoteEditorPage {
|
|||||||
this.richTextEditor = this.containerLocator.locator('iframe[title="Rich Text Area"]');
|
this.richTextEditor = this.containerLocator.locator('iframe[title="Rich Text Area"]');
|
||||||
this.noteTitleInput = this.containerLocator.locator('.title-input');
|
this.noteTitleInput = this.containerLocator.locator('.title-input');
|
||||||
this.attachFileButton = this.containerLocator.getByRole('button', { name: 'Attach file' });
|
this.attachFileButton = this.containerLocator.getByRole('button', { name: 'Attach file' });
|
||||||
|
this.toggleCodeBlockButton = this.containerLocator.getByRole('button', { name: 'Code Block' });
|
||||||
this.toggleEditorsButton = this.containerLocator.getByRole('button', { name: 'Toggle editors' });
|
this.toggleEditorsButton = this.containerLocator.getByRole('button', { name: 'Toggle editors' });
|
||||||
this.toggleEditorLayoutButton = this.containerLocator.getByRole('button', { name: 'Toggle editor layout' });
|
this.toggleEditorLayoutButton = this.containerLocator.getByRole('button', { name: 'Toggle editor layout' });
|
||||||
this.noteViewerContainer = this.containerLocator.locator('iframe[src$="note-viewer/index.html"]');
|
this.noteViewerContainer = this.containerLocator.locator('iframe[src$="note-viewer/index.html"]');
|
||||||
// The editor and viewer have slightly different search UI
|
// The editor and viewer have slightly different search UI
|
||||||
this.editorSearchInput = this.containerLocator.getByPlaceholder('Find');
|
this.editorSearchInput = this.containerLocator.getByPlaceholder('Find');
|
||||||
this.viewerSearchInput = this.containerLocator.getByPlaceholder('Search...');
|
this.viewerSearchInput = this.containerLocator.getByPlaceholder('Search...');
|
||||||
|
this.disableTabNavigationButton = this.containerLocator.getByRole('button', { name: 'Tab moves focus' });
|
||||||
|
|
||||||
|
this.richTextCodeEditor = new EditorCodeDialog(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
public toolbarButtonLocator(title: string) {
|
public toolbarButtonLocator(title: string) {
|
||||||
@@ -75,6 +87,18 @@ export default class NoteEditorPage {
|
|||||||
return this.codeMirrorEditor.click();
|
return this.codeMirrorEditor.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async enableTabNavigation(electronApp: ElectronApplication) {
|
||||||
|
await expect(this.disableTabNavigationButton).not.toBeVisible();
|
||||||
|
await activateMainMenuItem(electronApp, 'Tab moves focus');
|
||||||
|
await expect(this.disableTabNavigationButton).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disableTabNavigation(electronApp: ElectronApplication) {
|
||||||
|
await expect(this.disableTabNavigationButton).toBeVisible();
|
||||||
|
await activateMainMenuItem(electronApp, 'Tab moves focus');
|
||||||
|
await expect(this.disableTabNavigationButton).not.toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
public async waitFor() {
|
public async waitFor() {
|
||||||
await this.noteTitleInput.waitFor();
|
await this.noteTitleInput.waitFor();
|
||||||
await this.toggleEditorsButton.waitFor();
|
await this.toggleEditorsButton.waitFor();
|
||||||
|
|||||||
@@ -120,6 +120,56 @@ test.describe('richTextEditor', () => {
|
|||||||
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 disable tab indentation from the menu', async ({ mainWindow, electronApp }) => {
|
||||||
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||||
|
await mainScreen.createNewNote('Testing keyboard navigation!');
|
||||||
|
|
||||||
|
const editor = mainScreen.noteEditor;
|
||||||
|
await editor.toggleEditorsButton.click();
|
||||||
|
await editor.richTextEditor.click();
|
||||||
|
|
||||||
|
await editor.enableTabNavigation(electronApp);
|
||||||
|
await mainWindow.keyboard.type('This is a');
|
||||||
|
|
||||||
|
// Tab should navigate
|
||||||
|
await expect(editor.richTextEditor).toBeFocused();
|
||||||
|
await mainWindow.keyboard.press('Tab');
|
||||||
|
await expect(editor.richTextEditor).not.toBeFocused();
|
||||||
|
|
||||||
|
await editor.disableTabNavigation(electronApp);
|
||||||
|
|
||||||
|
// Tab should not navigate
|
||||||
|
await editor.richTextEditor.click();
|
||||||
|
await mainWindow.keyboard.press('Tab');
|
||||||
|
await expect(editor.richTextEditor).toBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disabling tab indentation should also disable it in code dialogs', async ({ mainWindow, electronApp }) => {
|
||||||
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||||
|
await mainScreen.createNewNote('Testing code blocks');
|
||||||
|
|
||||||
|
const editor = mainScreen.noteEditor;
|
||||||
|
await editor.toggleEditorsButton.click();
|
||||||
|
await editor.richTextEditor.click();
|
||||||
|
|
||||||
|
await editor.toggleCodeBlockButton.click();
|
||||||
|
const codeEditor = editor.richTextCodeEditor;
|
||||||
|
await codeEditor.waitFor();
|
||||||
|
|
||||||
|
// Initially, pressing <tab> in the textarea should add a tab
|
||||||
|
await codeEditor.textArea.click();
|
||||||
|
await mainWindow.keyboard.press('Tab');
|
||||||
|
await expect(codeEditor.textArea).toHaveValue('\t');
|
||||||
|
await expect(codeEditor.textArea).toBeFocused();
|
||||||
|
|
||||||
|
await editor.enableTabNavigation(electronApp);
|
||||||
|
|
||||||
|
// After enabling tab navigation, pressing tab should navigate.
|
||||||
|
await expect(codeEditor.textArea).toBeFocused();
|
||||||
|
await mainWindow.keyboard.press('Tab');
|
||||||
|
await expect(codeEditor.textArea).not.toBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
test('should be possible to navigate between the note title and rich text editor with enter/down/up keys', async ({ mainWindow }) => {
|
test('should be possible to navigate between the note title and rich text editor with enter/down/up keys', async ({ mainWindow }) => {
|
||||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||||
await mainScreen.createNewNote('Testing keyboard navigation!');
|
await mainScreen.createNewNote('Testing keyboard navigation!');
|
||||||
|
|||||||
@@ -347,6 +347,8 @@ function NoteEditor(props: Props, ref: any) {
|
|||||||
ignoreModifiers: false,
|
ignoreModifiers: false,
|
||||||
autocompleteMarkup: Setting.value('editor.autocompleteMarkup'),
|
autocompleteMarkup: Setting.value('editor.autocompleteMarkup'),
|
||||||
|
|
||||||
|
// For now, mobile CodeMirror uses its built-in focus toggle shortcut.
|
||||||
|
tabMovesFocus: false,
|
||||||
indentWithTabs: true,
|
indentWithTabs: true,
|
||||||
|
|
||||||
editorLabel: _('Markdown editor'),
|
editorLabel: _('Markdown editor'),
|
||||||
|
|||||||
@@ -181,6 +181,10 @@ const createEditor = (
|
|||||||
keyCommand('Mod-]', increaseIndent),
|
keyCommand('Mod-]', increaseIndent),
|
||||||
keyCommand('Mod-k', showLinkEditor),
|
keyCommand('Mod-k', showLinkEditor),
|
||||||
keyCommand('Tab', (view: EditorView) => {
|
keyCommand('Tab', (view: EditorView) => {
|
||||||
|
if (settings.tabMovesFocus) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (settings.autocompleteMarkup) {
|
if (settings.autocompleteMarkup) {
|
||||||
return insertOrIncreaseIndent(view);
|
return insertOrIncreaseIndent(view);
|
||||||
}
|
}
|
||||||
@@ -188,6 +192,10 @@ const createEditor = (
|
|||||||
return insertTab(view);
|
return insertTab(view);
|
||||||
}, true),
|
}, true),
|
||||||
keyCommand('Shift-Tab', (view) => {
|
keyCommand('Shift-Tab', (view) => {
|
||||||
|
if (settings.tabMovesFocus) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// When at the beginning of the editor, allow shift-tab to act
|
// When at the beginning of the editor, allow shift-tab to act
|
||||||
// normally.
|
// normally.
|
||||||
if (isCursorAtBeginning(view.state)) {
|
if (isCursorAtBeginning(view.state)) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const createEditorSettings = (themeId: number) => {
|
|||||||
automatchBraces: false,
|
automatchBraces: false,
|
||||||
ignoreModifiers: false,
|
ignoreModifiers: false,
|
||||||
autocompleteMarkup: true,
|
autocompleteMarkup: true,
|
||||||
|
tabMovesFocus: false,
|
||||||
|
|
||||||
keymap: EditorKeymap.Default,
|
keymap: EditorKeymap.Default,
|
||||||
language: EditorLanguageType.Markdown,
|
language: EditorLanguageType.Markdown,
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ export interface EditorSettings {
|
|||||||
language: EditorLanguageType;
|
language: EditorLanguageType;
|
||||||
|
|
||||||
keymap: EditorKeymap;
|
keymap: EditorKeymap;
|
||||||
|
tabMovesFocus: boolean;
|
||||||
|
|
||||||
katexEnabled: boolean;
|
katexEnabled: boolean;
|
||||||
spellcheckEnabled: boolean;
|
spellcheckEnabled: boolean;
|
||||||
|
|||||||
@@ -683,6 +683,16 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
|||||||
appTypes: [AppType.Mobile],
|
appTypes: [AppType.Mobile],
|
||||||
label: () => 'buttons included in the editor toolbar',
|
label: () => 'buttons included in the editor toolbar',
|
||||||
},
|
},
|
||||||
|
'editor.tabMovesFocus': {
|
||||||
|
value: false,
|
||||||
|
type: SettingItemType.Bool,
|
||||||
|
public: false,
|
||||||
|
section: 'note',
|
||||||
|
appTypes: [AppType.Desktop],
|
||||||
|
label: () => _('Tab moves focus'),
|
||||||
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
|
},
|
||||||
'notes.columns': {
|
'notes.columns': {
|
||||||
value: defaultListColumns(),
|
value: defaultListColumns(),
|
||||||
public: false,
|
public: false,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const defaultKeymapItems = {
|
|||||||
{ accelerator: 'Option+Cmd+3', command: 'switchProfile3' },
|
{ accelerator: 'Option+Cmd+3', command: 'switchProfile3' },
|
||||||
{ accelerator: 'Option+Cmd+Backspace', command: 'permanentlyDeleteNote' },
|
{ accelerator: 'Option+Cmd+Backspace', command: 'permanentlyDeleteNote' },
|
||||||
{ accelerator: 'Option+Cmd+N', command: 'openNoteInNewWindow' },
|
{ accelerator: 'Option+Cmd+N', command: 'openNoteInNewWindow' },
|
||||||
|
{ accelerator: 'Ctrl+M', command: 'toggleTabMovesFocus' },
|
||||||
],
|
],
|
||||||
default: [
|
default: [
|
||||||
{ accelerator: 'Ctrl+N', command: 'newNote' },
|
{ accelerator: 'Ctrl+N', command: 'newNote' },
|
||||||
@@ -110,6 +111,7 @@ const defaultKeymapItems = {
|
|||||||
{ accelerator: 'Ctrl+Alt+2', command: 'switchProfile2' },
|
{ accelerator: 'Ctrl+Alt+2', command: 'switchProfile2' },
|
||||||
{ accelerator: 'Ctrl+Alt+3', command: 'switchProfile3' },
|
{ accelerator: 'Ctrl+Alt+3', command: 'switchProfile3' },
|
||||||
{ accelerator: 'Ctrl+Alt+N', command: 'openNoteInNewWindow' },
|
{ accelerator: 'Ctrl+Alt+N', command: 'openNoteInNewWindow' },
|
||||||
|
{ accelerator: 'Ctrl+M', command: 'toggleTabMovesFocus' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -417,6 +419,13 @@ export default class KeymapService extends BaseService {
|
|||||||
return parts.join('+');
|
return parts.join('+');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Electron and aria-keyshortcuts have slightly different formats for accelerators.
|
||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-keyshortcuts
|
||||||
|
public getAriaKeyShortcuts(commandName: string) {
|
||||||
|
const electronAccelerator = this.getAccelerator(commandName);
|
||||||
|
return electronAccelerator.replace('Ctrl', 'Control');
|
||||||
|
}
|
||||||
|
|
||||||
public on<Name extends EventName>(eventName: Name, callback: EventListenerCallback<Name>) {
|
public on<Name extends EventName>(eventName: Name, callback: EventListenerCallback<Name>) {
|
||||||
eventManager.on(eventName, callback);
|
eventManager.on(eventName, callback);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,3 +160,4 @@ Tebi
|
|||||||
unwatcher
|
unwatcher
|
||||||
pedr
|
pedr
|
||||||
Slotozilla
|
Slotozilla
|
||||||
|
keyshortcuts
|
||||||
|
|||||||
Reference in New Issue
Block a user