mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
This commit is contained in:
parent
72163018b4
commit
b94cf5a107
@ -299,6 +299,7 @@ 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/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/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
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -276,6 +276,7 @@ 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/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/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
|
||||||
|
@ -38,6 +38,7 @@ const md5 = require('md5');
|
|||||||
const { clipboard } = require('electron');
|
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';
|
||||||
|
|
||||||
const logger = Logger.create('TinyMCE');
|
const logger = Logger.create('TinyMCE');
|
||||||
|
|
||||||
@ -128,6 +129,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);
|
||||||
|
|
||||||
// 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) => {
|
||||||
@ -629,6 +631,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
|||||||
icons_url: 'gui/NoteEditor/NoteBody/TinyMCE/icons.js',
|
icons_url: 'gui/NoteEditor/NoteBody/TinyMCE/icons.js',
|
||||||
plugins: 'noneditable link joplinLists hr searchreplace codesample table',
|
plugins: 'noneditable link joplinLists hr searchreplace codesample table',
|
||||||
noneditable_noneditable_class: 'joplin-editable', // Can be a regex too
|
noneditable_noneditable_class: 'joplin-editable', // Can be a regex too
|
||||||
|
iframe_aria_text: _('Rich Text editor. Press Escape then Tab to escape focus.'),
|
||||||
|
|
||||||
// #p: Pad empty paragraphs with to prevent them from being removed.
|
// #p: Pad empty paragraphs with to prevent them from being removed.
|
||||||
// *[*]: Allow all elements and attributes -- we already filter in sanitize_html
|
// *[*]: Allow all elements and attributes -- we already filter in sanitize_html
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import type { Editor, EditorEvent } from 'tinymce';
|
||||||
|
|
||||||
|
const useTabIndenter = (editor: Editor) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor) return () => {};
|
||||||
|
|
||||||
|
const canChangeIndentation = () => {
|
||||||
|
const selectionElement = editor.selection.getNode();
|
||||||
|
// List items and tables have their own tab key handlers.
|
||||||
|
return !selectionElement.closest('li, table') && !editor.readonly;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSpacesBeforeSelectionRange = (maxLength: number) => {
|
||||||
|
const selectionRange = editor.selection.getRng();
|
||||||
|
|
||||||
|
let rangeStart = selectionRange.startOffset;
|
||||||
|
let outputRange = selectionRange.cloneRange();
|
||||||
|
while (rangeStart >= 0) {
|
||||||
|
rangeStart--;
|
||||||
|
|
||||||
|
const lastRange = outputRange.cloneRange();
|
||||||
|
outputRange.setStart(outputRange.startContainer, Math.max(rangeStart, 0));
|
||||||
|
const rangeContent = outputRange.toString();
|
||||||
|
const isWhitespace = rangeContent.match(/^\s*$/);
|
||||||
|
if (!isWhitespace || rangeContent.length > maxLength) {
|
||||||
|
outputRange = lastRange;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputRange;
|
||||||
|
};
|
||||||
|
|
||||||
|
const indentLengthChars = 8;
|
||||||
|
let indentHtml = '';
|
||||||
|
for (let i = 0; i < indentLengthChars; i++) {
|
||||||
|
indentHtml += ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastKeyWasEscape = false;
|
||||||
|
|
||||||
|
const eventHandler = (event: EditorEvent<KeyboardEvent>) => {
|
||||||
|
if (!event.isDefaultPrevented() && event.key === 'Tab' && canChangeIndentation() && !lastKeyWasEscape) {
|
||||||
|
if (!event.shiftKey) {
|
||||||
|
editor.execCommand('mceInsertContent', false, indentHtml);
|
||||||
|
event.preventDefault();
|
||||||
|
} else {
|
||||||
|
const selectionRange = editor.selection.getRng();
|
||||||
|
if (selectionRange.collapsed) {
|
||||||
|
const spacesRange = getSpacesBeforeSelectionRange(indentLengthChars);
|
||||||
|
|
||||||
|
const hasAtLeastOneSpace = spacesRange.toString().match(/^\s+$/);
|
||||||
|
if (hasAtLeastOneSpace) {
|
||||||
|
editor.selection.setRng(spacesRange);
|
||||||
|
editor.execCommand('Delete', false);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event.key === 'Escape' && !event.shiftKey && !event.altKey && !event.metaKey && !event.ctrlKey) {
|
||||||
|
// For accessibility, let Escape followed by tab escape the focus trap.
|
||||||
|
lastKeyWasEscape = true;
|
||||||
|
} else if (event.key !== 'Shift') { // Allows Esc->Shift+Tab to escape the focus trap.
|
||||||
|
lastKeyWasEscape = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.on('keydown', eventHandler);
|
||||||
|
return () => {
|
||||||
|
editor.off('keydown', eventHandler);
|
||||||
|
};
|
||||||
|
}, [editor]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTabIndenter;
|
@ -82,5 +82,42 @@ test.describe('richTextEditor', () => {
|
|||||||
expect(await openPathResult).toContain(basename(pathToAttach));
|
expect(await openPathResult).toContain(basename(pathToAttach));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('pressing Tab should indent', async ({ mainWindow }) => {
|
||||||
|
const mainScreen = new MainScreen(mainWindow);
|
||||||
|
await mainScreen.createNewNote('Testing tabs!');
|
||||||
|
const editor = mainScreen.noteEditor;
|
||||||
|
|
||||||
|
await editor.toggleEditorsButton.click();
|
||||||
|
await editor.richTextEditor.click();
|
||||||
|
|
||||||
|
await mainWindow.keyboard.type('This is a');
|
||||||
|
// Tab should add spaces
|
||||||
|
await mainWindow.keyboard.press('Tab');
|
||||||
|
await mainWindow.keyboard.type('test.');
|
||||||
|
|
||||||
|
// Shift-tab should remove spaces
|
||||||
|
await mainWindow.keyboard.press('Tab');
|
||||||
|
await mainWindow.keyboard.press('Tab');
|
||||||
|
await mainWindow.keyboard.press('Shift+Tab');
|
||||||
|
await mainWindow.keyboard.type('Test!');
|
||||||
|
|
||||||
|
// Escape then tab should move focus
|
||||||
|
await mainWindow.keyboard.press('Escape');
|
||||||
|
await expect(editor.richTextEditor).toBeFocused();
|
||||||
|
await mainWindow.keyboard.press('Tab');
|
||||||
|
await expect(editor.richTextEditor).not.toBeFocused();
|
||||||
|
|
||||||
|
// After re-focusing the editor, Tab should indent again.
|
||||||
|
await mainWindow.keyboard.press('Shift+Tab');
|
||||||
|
await expect(editor.richTextEditor).toBeFocused();
|
||||||
|
await mainWindow.keyboard.type(' Another:');
|
||||||
|
await mainWindow.keyboard.press('Tab');
|
||||||
|
await mainWindow.keyboard.type('!');
|
||||||
|
|
||||||
|
// After switching back to the Markdown editor,
|
||||||
|
await expect(editor.toggleEditorsButton).not.toBeDisabled();
|
||||||
|
await editor.toggleEditorsButton.click();
|
||||||
|
await expect(editor.codeMirrorEditor).toHaveText('This is a test. Test! Another: !');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user