Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/useLinkTooltips.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/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/useLinkTooltips.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/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const md5 = require('md5');
const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales');
import { hasProtocol } from '@joplin/utils/url';
import useTabIndenter from './utils/useTabIndenter';

const logger = Logger.create('TinyMCE');

Expand Down Expand Up @@ -128,6 +129,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {

usePluginServiceRegistration(ref);
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
const dispatchDidUpdate = (editor: any) => {
Expand Down Expand Up @@ -629,6 +631,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
icons_url: 'gui/NoteEditor/NoteBody/TinyMCE/icons.js',
plugins: 'noneditable link joplinLists hr searchreplace codesample table',
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.
// *[*]: Allow all elements and attributes -- we already filter in sanitize_html
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
37 changes: 37 additions & 0 deletions packages/app-desktop/integration-tests/richTextEditor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,42 @@ test.describe('richTextEditor', () => {
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