From 5782ee6ba19c279cb6377f3b5b756ffde22cb453 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Sat, 18 Jan 2025 04:37:46 -0800 Subject: [PATCH] Desktop: Upgrade to TinyMCE v6 (#11652) --- .../JoplinLists/src/main/ts/core/Selection.ts | 5 +- .../JoplinLists/src/main/ts/ui/Buttons.ts | 3 +- .../NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx | 59 ++++++++++++------- .../NoteBody/TinyMCE/plugins/lists.js | 7 +-- .../integration-tests/wcag.spec.ts | 7 +++ packages/app-desktop/package.json | 2 +- packages/tools/cspell/dictionary4.txt | 2 +- yarn.lock | 10 ++-- 8 files changed, 57 insertions(+), 38 deletions(-) diff --git a/Assets/TinyMCE/JoplinLists/src/main/ts/core/Selection.ts b/Assets/TinyMCE/JoplinLists/src/main/ts/core/Selection.ts index 0a871bdd6..4ce8f135f 100644 --- a/Assets/TinyMCE/JoplinLists/src/main/ts/core/Selection.ts +++ b/Assets/TinyMCE/JoplinLists/src/main/ts/core/Selection.ts @@ -8,7 +8,6 @@ import { Node } from '@ephox/dom-globals'; import { Arr, Option } from '@ephox/katamari'; import { HTMLElement } from '@ephox/sand'; -import DomQuery from 'tinymce/core/api/dom/DomQuery'; import Editor from 'tinymce/core/api/Editor'; import Tools from 'tinymce/core/api/util/Tools'; import * as NodeType from './NodeType'; @@ -49,7 +48,7 @@ const findParentListItemsNodes = function (editor, elms) { return parentLi ? parentLi : elm; }); - return DomQuery.unique(listItemsElms); + return [...new Set(listItemsElms)]; }; const getSelectedListItems = function (editor) { @@ -89,7 +88,7 @@ const getSelectedListRoots = (editor: Editor): Node[] => { const getUniqueListRoots = (editor: Editor, lists: Node[]): Node[] => { const listRoots = Arr.map(lists, (list) => findLastParentListNode(editor, list).getOr(list)); - return DomQuery.unique(listRoots); + return [...new Set(listRoots)]; }; const isList = (editor: Editor): boolean => { diff --git a/Assets/TinyMCE/JoplinLists/src/main/ts/ui/Buttons.ts b/Assets/TinyMCE/JoplinLists/src/main/ts/ui/Buttons.ts index 1e48b3832..cc07e824a 100644 --- a/Assets/TinyMCE/JoplinLists/src/main/ts/ui/Buttons.ts +++ b/Assets/TinyMCE/JoplinLists/src/main/ts/ui/Buttons.ts @@ -48,8 +48,7 @@ const listState = function (editor: Editor, listName, options:any = {}) { const register = function (editor: Editor) { const hasPlugin = function (editor, plugin) { - const plugins = editor.settings.plugins ? editor.settings.plugins : ''; - return Tools.inArray(plugins.split(/[ ,]/), plugin) !== -1; + return editor.hasPlugin(plugin); }; const _ = Settings.getLocalizationFunction(editor); diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx index 26be0fd39..1521f9ac6 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx @@ -24,7 +24,7 @@ import { themeStyle } from '@joplin/lib/theme'; import { loadScript } from '../../../utils/loadScript'; import bridge from '../../../../services/bridge'; import { TinyMceEditorEvents } from './utils/types'; -import type { Editor } from 'tinymce'; +import type { Editor, EditorEvent } from 'tinymce'; import { joplinCommandToTinyMceCommands, TinyMceCommand } from './utils/joplinCommandToTinyMceCommands'; import shouldPasteResources from './utils/shouldPasteResources'; import lightTheme from '@joplin/lib/themes/light'; @@ -412,9 +412,11 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { element.setAttribute('id', 'tinyMceStyle'); editorContainerDom.head.appendChild(element); element.appendChild(editorContainerDom.createTextNode(` - .joplin-tinymce .tox-editor-header { - padding-left: ${styles.leftExtraToolbarContainer.width + styles.leftExtraToolbarContainer.padding * 2}px; - padding-right: ${styles.rightExtraToolbarContainer.width + styles.rightExtraToolbarContainer.padding * 2}px; + .joplin-tinymce .tox-editor-header.tox-editor-header { + margin-left: ${styles.leftExtraToolbarContainer.width + styles.leftExtraToolbarContainer.padding * 2}px; + margin-right: ${styles.rightExtraToolbarContainer.width + styles.rightExtraToolbarContainer.padding * 2}px; + padding: 0; + box-shadow: none; } .tox .tox-toolbar, @@ -434,7 +436,8 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { } .tox .tox-dialog__body-content, - .tox .tox-collection__item { + .tox .tox-collection__item, + .tox .tox-insert-table-picker__label { color: ${theme.color}; } @@ -473,7 +476,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { */ .tox .tox-dialog textarea { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-family: Menlo, Monaco, Consolas, "Courier New", monospace !important; } .tox .tox-dialog-wrap__backdrop { @@ -498,6 +501,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { .tox .tox-toolbar-label { color: ${theme.color3} !important; fill: ${theme.color3} !important; + background: transparent; } .tox .tox-statusbar a, @@ -524,6 +528,11 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { .tox .tox-split-button:focus { background-color: ${theme.backgroundColor3} } + + .tox .tox-tbtn:focus-visible, + .tox .tox-split-button:focus-visible { + background-color: ${theme.backgroundColorHover3} + } .tox .tox-tbtn:hover, .tox .tox-menu button:hover > svg { @@ -560,6 +569,12 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { .tox .tox-split-button:hover { box-shadow: none; } + + /* Decrease the spacing between groups */ + .tox .tox-toolbar__group { + padding-left: 7px; + padding-right: 7px; + } .tox-tinymce, .tox .tox-toolbar__group, @@ -628,7 +643,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { useEffect(() => { if (!editor) return; - editor.setMode(props.disabled ? 'readonly' : 'design'); + editor.mode.set(props.disabled ? 'readonly' : 'design'); }, [editor, props.disabled]); // ----------------------------------------------------------------------------------------- @@ -675,14 +690,19 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { const containerWindow = editorContainerDom.defaultView as any; const editors = await containerWindow.tinymce.init({ selector: `#${editorContainer.id}`, + + // Ensures that the "Premium plugins" toolbar option is disabled. See + // https://www.tiny.cloud/docs/tinymce/latest/editor-premium-upgrade-promotion/ + promotion: false, width: '100%', body_class: 'jop-tinymce', height: '100%', resize: false, + highlight_on_focus: false, icons: 'Joplin', 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 + plugins: 'link joplinLists searchreplace codesample table', + 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. @@ -694,7 +714,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { relative_urls: false, branding: false, statusbar: false, - target_list: false, + link_target_list: false, // Handle the first table row as table header. // https://www.tiny.cloud/docs/plugins/table/#table_header_type table_header_type: 'sectionCells', @@ -704,13 +724,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { contextmenu: false, browser_spellcheck: true, - // Work around an issue where images with a base64 SVG data URL would be broken. - // - // See https://github.com/tinymce/tinymce/issues/3864 - // - // This was fixed in TinyMCE 6.1, so remove it when we upgrade. - images_dataimg_filter: (img: HTMLImageElement) => !img.src.startsWith('data:'), - formats: { joplinHighlight: { inline: 'mark', remove: 'all' }, joplinStrikethrough: { inline: 's', remove: 'all' }, @@ -747,14 +760,15 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { tooltip: _('Inline Code'), icon: 'sourcecode', onAction: function() { - editor.execCommand('mceToggleFormat', false, 'code', { class: 'inline-code' }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + editor.execCommand('mceToggleFormat', false, 'code', { class: 'inline-code' } as any); }, onSetup: function(api) { api.setActive(editor.formatter.match('code')); - const unbind = editor.formatter.formatChanged('code', api.setActive).unbind; + const handle = editor.formatter.formatChanged('code', active => api.setActive(active)); return function() { - if (unbind) unbind(); + handle?.unbind(); }; }, }); @@ -1206,9 +1220,10 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const onSetAttrib = (event: any) => { + const onSetAttrib = (event: EditorEvent) => { // Dispatch onChange when a link is edited - if (event.attrElm[0].nodeName === 'A') { + const target = Array.isArray(event.attrElm) ? event.attrElm[0] : event.attrElm; + if (target.nodeName === 'A') { if (event.attrName === 'title' || event.attrName === 'href' || event.attrName === 'rel') { onChangeHandler(); } diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js index 25a6b3331..2d3a4c98e 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js @@ -886,7 +886,7 @@ var parentLi = editor.dom.getParent(elm, 'li,dd,dt', getClosestListRootElm(editor, elm)); return parentLi ? parentLi : elm; }); - return DomQuery.unique(listItemsElms); + return [...new Set(listItemsElms)]; }; var getSelectedListItems = function (editor) { var selectedBlocks = editor.selection.getSelectedBlocks(); @@ -919,7 +919,7 @@ var listRoots = map(lists, function (list) { return findLastParentListNode(editor, list).getOr(list); }); - return DomQuery.unique(listRoots); + return [...new Set(listRoots)]; }; var shouldIndentOnTab = function (editor) { @@ -2119,8 +2119,7 @@ }; var register$1 = function (editor) { var hasPlugin = function (editor, plugin) { - var plugins = editor.settings.plugins ? editor.settings.plugins : ''; - return Tools.inArray(plugins.split(/[ ,]/), plugin) !== -1; + return editor.hasPlugin(plugin); }; var _ = getLocalizationFunction(editor); var exec = function (command) { diff --git a/packages/app-desktop/integration-tests/wcag.spec.ts b/packages/app-desktop/integration-tests/wcag.spec.ts index 940e5b4ae..4caaef4b3 100644 --- a/packages/app-desktop/integration-tests/wcag.spec.ts +++ b/packages/app-desktop/integration-tests/wcag.spec.ts @@ -7,6 +7,7 @@ import { Page } from '@playwright/test'; const createScanner = (page: Page) => { return new AxeBuilder({ page }) .disableRules(['page-has-heading-one']) + // Needed because we're using Electron. See https://github.com/dequelabs/axe-core-npm/issues/1141 .setLegacyMode(true); }; @@ -63,6 +64,12 @@ test.describe('wcag', () => { await mainScreen.noteEditor.noteTitleInput.hover(); await expectNoViolations(mainWindow); + + // Should not find issues with the Rich Text Editor + await mainScreen.noteEditor.toggleEditorsButton.click(); + await mainScreen.noteEditor.richTextEditor.click(); + + await expectNoViolations(mainWindow); }); }); diff --git a/packages/app-desktop/package.json b/packages/app-desktop/package.json index 787766244..f5815c37f 100644 --- a/packages/app-desktop/package.json +++ b/packages/app-desktop/package.json @@ -208,6 +208,6 @@ "styled-system": "5.1.5", "taboverride": "4.0.3", "tesseract.js": "5.1.0", - "tinymce": "5.10.6" + "tinymce": "6.8.5" } } diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index 5ce3c3ebb..e94d0e773 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -158,4 +158,4 @@ ldaps Bluesky Tebi unwatcher -pedr \ No newline at end of file +pedr diff --git a/yarn.lock b/yarn.lock index 76b671e22..cbb487c02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8340,7 +8340,7 @@ __metadata: styled-system: 5.1.5 taboverride: 4.0.3 tesseract.js: 5.1.0 - tinymce: 5.10.6 + tinymce: 6.8.5 ts-jest: 29.1.5 ts-node: 10.9.2 typescript: 5.4.5 @@ -45807,10 +45807,10 @@ __metadata: languageName: node linkType: hard -"tinymce@npm:5.10.6": - version: 5.10.6 - resolution: "tinymce@npm:5.10.6" - checksum: 806cd733fde872f97d84b22ed267d2ee0404aa003d89b6c295b91867ac8b2bafc99542bf04924f253cf093c920a9da4b37a7ef73a00ff57b2b75490fb9705caf +"tinymce@npm:6.8.5": + version: 6.8.5 + resolution: "tinymce@npm:6.8.5" + checksum: 7f7ad8dd2b117b8a671f97e41fc094935cfe4d4b525c90e97c6fdb480b19514e334f4360d89f34c04915a928e0cf5264fc1a60559554770452d6fce17b884940 languageName: node linkType: hard