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: Accessibility: Rich Text Editor: Make it possible to edit code blocks with a keyboard or touchscreen #11727

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a5373fa
Desktop: Accessibility: Make ctrl-m toggle tab-key indentation
personalizedrefrigerator Jan 23, 2025
f25f7bf
Allow configuring the shortcut
personalizedrefrigerator Jan 23, 2025
a3e833c
Show a status notification when tab indentation is disabled
personalizedrefrigerator Jan 23, 2025
d00659f
Use a command for toggling tab focus
personalizedrefrigerator Jan 23, 2025
2c5a5a1
Refactoring, update status indicator UI
personalizedrefrigerator Jan 23, 2025
d86c434
Move shortcut to the view menu
personalizedrefrigerator Jan 23, 2025
cea37e9
Fix menu item check status
personalizedrefrigerator Jan 23, 2025
d2f71ab
Tests
personalizedrefrigerator Jan 23, 2025
bba4fe0
Fix tsc: Missing property
personalizedrefrigerator Jan 23, 2025
5889ff5
Desktop: Accessibility: Make it possible to edit code blocks without a
personalizedrefrigerator Jan 25, 2025
4c96b8e
Fix: Also apply tab navigation setting to Rich Text Editor dialogs
personalizedrefrigerator Jan 25, 2025
e9c7b70
Add test for tab indentation toggling in dialogs
personalizedrefrigerator Jan 25, 2025
10e8953
Improve function naming
personalizedrefrigerator Jan 25, 2025
d8f7043
Merge branch 'pr/desktop/accessibility/tab-key-moves-focus' into pr/d…
personalizedrefrigerator Jan 25, 2025
e3f5b20
Add an "Edit" item to the context menu
personalizedrefrigerator Jan 25, 2025
1f5091d
Refactoring
personalizedrefrigerator Jan 25, 2025
0a621ff
Regression test
personalizedrefrigerator Jan 25, 2025
408cfb2
Merge branch 'dev' into pr/desktop/accessibility/tab-key-moves-focus
personalizedrefrigerator Jan 27, 2025
6b027a3
Merge branch 'dev' into pr/desktop/accessibility/rte-edit-code-right-…
personalizedrefrigerator Jan 27, 2025
4e0b811
updateIgnored
personalizedrefrigerator Jan 27, 2025
5dad026
Update .gitignore, .eslintignore
personalizedrefrigerator Jan 27, 2025
7326f18
Refactoring: Rename command for consistency, refactor CSS
personalizedrefrigerator Jan 27, 2025
c11539f
Merge branch 'pr/desktop/accessibility/tab-key-moves-focus' into pr/d…
personalizedrefrigerator Jan 27, 2025
22fd217
Merge remote-tracking branch 'upstream/dev' into pr/desktop/accessibi…
personalizedrefrigerator Jan 27, 2025
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
4 changes: 3 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,15 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVis
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/enableTextAreaTab.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.test.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/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialogEventListeners.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/useScroll.js
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,15 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVis
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/enableTextAreaTab.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.test.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/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialogEventListeners.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/useScroll.js
Expand Down
40 changes: 16 additions & 24 deletions packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
import BaseItem from '@joplin/lib/models/BaseItem';
import setupToolbarButtons from './utils/setupToolbarButtons';
import { plainTextToHtml } from '@joplin/lib/htmlUtils';
import openEditDialog from './utils/openEditDialog';
import { themeStyle } from '@joplin/lib/theme';
import { loadScript } from '../../../utils/loadScript';
import bridge from '../../../../services/bridge';
Expand All @@ -42,6 +41,8 @@ import { hasProtocol } from '@joplin/utils/url';
import useTabIndenter from './utils/useTabIndenter';
import useKeyboardRefocusHandler from './utils/useKeyboardRefocusHandler';
import useDocument from '../../../hooks/useDocument';
import useEditDialog from './utils/useEditDialog';
import useEditDialogEventListeners from './utils/useEditDialogEventListeners';

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

Expand Down Expand Up @@ -72,14 +73,6 @@ function awfulInitHack(html: string): string {
return html === '<div id="rendered-md"></div>' ? '<div id="rendered-md"><p></p></div>' : html;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
function findEditableContainer(node: any): any {
while (node) {
if (node.classList && node.classList.contains('joplin-editable')) return node;
node = node.parentNode;
}
return null;
}

let markupToHtml_ = new MarkupToHtml();
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
Expand Down Expand Up @@ -130,19 +123,23 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {

const { scrollToPercent } = useScroll({ editor, onScroll: props.onScroll });

usePluginServiceRegistration(ref);
useContextMenu(editor, props.plugins, props.dispatch, props.htmlToMarkdown, props.markupToHtml);
useTabIndenter(editor, !props.tabMovesFocus);
useKeyboardRefocusHandler(editor);

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const dispatchDidUpdate = (editor: any) => {
const dispatchDidUpdate = useCallback((editor: Editor) => {
if (dispatchDidUpdateIID_) shim.clearTimeout(dispatchDidUpdateIID_);
dispatchDidUpdateIID_ = shim.setTimeout(() => {
dispatchDidUpdateIID_ = null;
if (editor && editor.getDoc()) editor.getDoc().dispatchEvent(new Event('joplin-noteDidUpdate'));
}, 10);
};
}, []);

const editDialog = useEditDialog({ editor, markupToHtml, dispatchDidUpdate });
const editDialogRef = useRef(editDialog);
editDialogRef.current = editDialog;

useEditDialogEventListeners(editor, editDialog);
usePluginServiceRegistration(ref);
useContextMenu(editor, props.plugins, props.dispatch, props.htmlToMarkdown, props.markupToHtml, editDialog);
useTabIndenter(editor, !props.tabMovesFocus);
useKeyboardRefocusHandler(editor);

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const insertResourcesIntoContent = useCallback(async (filePaths: string[] = null, options: any = null) => {
Expand Down Expand Up @@ -179,7 +176,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
props.onMessage({ channel: href });
}
}
}, [editor, props.onMessage]);
}, [editor, props.onMessage, dispatchDidUpdate]);

useImperativeHandle(ref, () => {
return {
Expand Down Expand Up @@ -752,7 +749,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
tooltip: _('Code Block'),
icon: 'code-sample',
onAction: async function() {
openEditDialog(editor, markupToHtml, dispatchDidUpdate, null);
editDialogRef.current.editNew();
},
});

Expand Down Expand Up @@ -819,11 +816,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
editor.addShortcut('Meta+Shift+9', '', () => editor.execCommand('InsertJoplinChecklist'));

// TODO: remove event on unmount?
editor.on('DblClick', (event) => {
const editable = findEditableContainer(event.target);
if (editable) openEditDialog(editor, markupToHtml, dispatchDidUpdate, editable);
});

editor.on('drop', (event) => {
// Prevent the message "Dropped file type is not supported" from showing up.
// It was added in TinyMCE 5.4 and doesn't apply since we do support
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Setting from '@joplin/lib/models/Setting';
import { focus } from '@joplin/lib/utils/focusHandler';
const taboverride = require('taboverride');

export interface TextAreaTabHandler {
remove(): void;
}

const createTextAreaKeyListeners = () => {
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 (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 };
};

// 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
// event listener will override the default behaviour, which is to focus the next field.
const enableTextAreaTab = (textAreas: HTMLTextAreaElement[]): TextAreaTabHandler => {
type RemoveCallback = ()=> void;
const removeCallbacks: RemoveCallback[] = [];

for (const textArea of textAreas) {
const { onKeyDown, onKeyUp } = createTextAreaKeyListeners();
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();
}
},
};
};

export default enableTextAreaTab;
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Editor } from 'tinymce';

// eslint-disable-next-line import/prefer-default-export
export enum TinyMceEditorEvents {
KeyUp = 'keyup',
Expand All @@ -14,3 +16,5 @@ export enum TinyMceEditorEvents {
ExecCommand = 'ExecCommand',
SetAttrib = 'SetAttrib',
}

export type DispatchDidUpdateCallback = (editor: Editor)=> void;
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import Resource from '@joplin/lib/models/Resource';
import { TinyMceEditorEvents } from './types';
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from '../../../utils/types';
import { Editor } from 'tinymce';
import { EditDialogControl } from './useEditDialog';
import { Dispatch } from 'redux';
import { _ } from '@joplin/lib/locale';

const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
Expand Down Expand Up @@ -52,23 +55,14 @@ interface ContextMenuActionOptions {

const contextMenuActionOptions: ContextMenuActionOptions = { current: null };

// 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(editor: Editor, plugins: PluginStates, dispatch: Function, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler) {
export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatch, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler, editDialog: EditDialogControl) {
useEffect(() => {
if (!editor) return () => {};

const contextMenuItems = menuItems(dispatch, htmlToMd, mdToHtml);
const targetWindow = bridge().activeWindow();

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
function onContextMenu(event: ElectronEvent, params: any) {
const element = contextMenuElement(editor, params.x, params.y);
if (!element) return;

event.preventDefault();

const menu = new Menu();

const makeMainMenuItems = (element: Element) => {
let itemType: ContextMenuItemType = ContextMenuItemType.None;
let resourceId = '';
let linkToCopy = null;
Expand Down Expand Up @@ -103,29 +97,57 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Functio
mdToHtml,
};

const result = [];
for (const itemName in contextMenuItems) {
const item = contextMenuItems[itemName];

if (!item.isActive(itemType, contextMenuActionOptions.current)) continue;

menu.append(new MenuItem({
result.push(new MenuItem({
label: item.label,
click: () => {
item.onAction(contextMenuActionOptions.current);
},
}));
}
return result;
};

const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);

for (const item of spellCheckerMenuItems) {
menu.append(new MenuItem(item));
const makeEditableMenuItems = (element: Element) => {
if (editDialog.isEditable(element)) {
return [
new MenuItem({
type: 'normal',
label: _('Edit'),
click: () => {
editDialog.editExisting(element);
},
}),
new MenuItem({ type: 'separator' }),
];
}
return [];
};

for (const item of menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu)) {
menu.append(new MenuItem(item));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
function onContextMenu(event: ElectronEvent, params: any) {
const element = contextMenuElement(editor, params.x, params.y);
if (!element) return;

event.preventDefault();

const menu = new Menu();
const menuItems = [];

menuItems.push(...makeEditableMenuItems(element));
menuItems.push(...makeMainMenuItems(element));
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
menuItems.push(...spellCheckerMenuItems);
menuItems.push(...menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu));

for (const item of menuItems) {
menu.append(item);
}
menu.popup({ window: targetWindow });
}

Expand All @@ -136,5 +158,5 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Functio
targetWindow.webContents.off('context-menu', onContextMenu);
}
};
}, [editor, plugins, dispatch, htmlToMd, mdToHtml]);
}, [editor, plugins, dispatch, htmlToMd, mdToHtml, editDialog]);
}
Loading
Loading