1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Desktop: Resolves #5762: Rich Text Editor: Add eight spaces when pressing tab (#10880)

This commit is contained in:
Henry Heino 2024-08-17 04:21:43 -07:00 committed by GitHub
parent 72163018b4
commit b94cf5a107
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 118 additions and 0 deletions

View File

@ -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
View File

@ -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

View File

@ -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

View File

@ -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 += '&nbsp;';
}
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;

View File

@ -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: !');
});
}); });