From b1877fcd0de0a0623cb8e3b4698136cadd100e5f Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Thu, 8 Feb 2024 12:51:31 +0000 Subject: [PATCH] Desktop: Speed up pasting text and images in Rich Text Editor --- .../NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx | 17 +++++++---- .../TinyMCE/utils/shouldPasteResources.ts | 2 +- .../gui/NoteEditor/utils/resourceHandling.ts | 28 ++++++++++++++----- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx index c46dc6573..c612f39b0 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx @@ -15,7 +15,6 @@ import useContextMenu from './utils/useContextMenu'; import { copyHtmlToClipboard } from '../../utils/clipboardUtils'; import shim from '@joplin/lib/shim'; import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer'; -import { reg } from '@joplin/lib/registry'; import BaseItem from '@joplin/lib/models/BaseItem'; import setupToolbarButtons from './utils/setupToolbarButtons'; import { plainTextToHtml } from '@joplin/lib/htmlUtils'; @@ -31,10 +30,13 @@ import lightTheme from '@joplin/lib/themes/light'; import { Options as NoteStyleOptions } from '@joplin/renderer/noteStyle'; import markupRenderOptions from '../../utils/markupRenderOptions'; import { DropHandler } from '../../utils/useDropHandler'; +import Logger from '@joplin/utils/Logger'; const md5 = require('md5'); const { clipboard } = require('electron'); const supportedLocales = require('./supportedLocales'); +const logger = Logger.create('TinyMCE'); + // In TinyMCE 5.2, when setting the body to '
', // it would end up as '

' once rendered // (an additional
was inserted). @@ -153,7 +155,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { if (anchor) { anchor.scrollIntoView(); } else { - reg.logger().warn('TinyMce: could not find anchor with ID ', anchorName); + logger.warn('could not find anchor with ID ', anchorName); } } else { props.onMessage({ channel: href }); @@ -193,7 +195,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { execCommand: async (cmd: EditorCommand) => { if (!editor) return false; - reg.logger().debug('TinyMce: execCommand', cmd); + logger.debug('execCommand', cmd); let commandProcessed = true; @@ -215,7 +217,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { } else if (cmd.value.type === 'files') { insertResourcesIntoContentRef.current(cmd.value.paths, { createFileURL: !!cmd.value.createFileURL }); } else { - reg.logger().warn('TinyMCE: unsupported drop item: ', cmd); + logger.warn('unsupported drop item: ', cmd); } } else { commandProcessed = false; @@ -249,7 +251,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { } if (!joplinCommandToTinyMceCommands[cmd.name]) { - reg.logger().warn('TinyMCE: unsupported Joplin command: ', cmd); + logger.warn('unsupported Joplin command: ', cmd); return false; } @@ -1132,16 +1134,20 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { const resourceMds = await getResourcesFromPasteEvent(event); if (shouldPasteResources(pastedText, pastedHtml, resourceMds)) { + logger.info(`onPaste: pasting ${resourceMds.length} resources`); if (resourceMds.length) { const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true })); editor.insertContent(result.html); } } else { if (BaseItem.isMarkdownTag(pastedText)) { // Paste a link to a note + logger.info('onPaste: pasting as a Markdown tag'); const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, pastedText, markupRenderOptions({ bodyOnly: true })); editor.insertContent(result.html); } else { // Paste regular text if (pastedHtml) { // Handles HTML + logger.info('onPaste: pasting as HTML'); + const modifiedHtml = await processPastedHtml( pastedHtml, prop_htmlToMarkdownRef.current, @@ -1149,6 +1155,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { ); editor.insertContent(modifiedHtml); } else { // Handles plain text + logger.info('onPaste: pasting as text'); pasteAsPlainText(pastedText); } } diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.ts index 7bc22f9f9..9549d712b 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.ts @@ -25,7 +25,7 @@ export default (pastedText: string, pastedHtml: string, resourceMds: string[]) = logger.info('Resources:', resourceMds); if (pastedText) { - logger.info('Not pasting resources because the clipboard contains plain text'); + logger.info('Not pasting resources only because the clipboard contains plain text'); return false; } diff --git a/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts b/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts index 836bddf13..1623eb954 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts @@ -152,8 +152,24 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa allImageUrls.push(src); }); + const downloadImage = async (imageSrc: string) => { + try { + const filePath = `${Setting.value('tempDir')}/${md5(Date.now() + Math.random())}`; + await shim.fetchBlob(imageSrc, { path: filePath }); + const createdResource = await shim.createResourceFromPath(filePath); + await shim.fsDriver().remove(filePath); + mappedResources[imageSrc] = `file://${encodeURI(Resource.fullPath(createdResource))}`; + } catch (error) { + logger.warn(`Error creating a resource for ${imageSrc}.`, error); + mappedResources[imageSrc] = imageSrc; + } + }; + + const downloadImages: Promise[] = []; + for (const imageSrc of allImageUrls) { if (!mappedResources[imageSrc]) { + logger.info(`processPastedHtml: Processing image ${imageSrc}`); try { if (imageSrc.startsWith('file')) { const imageFilePath = path.normalize(fileUriToPath(imageSrc)); @@ -165,22 +181,20 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa const createdResource = await shim.createResourceFromPath(imageFilePath); mappedResources[imageSrc] = `file://${encodeURI(Resource.fullPath(createdResource))}`; } - } else if (imageSrc.startsWith('data:')) { // Data URIs + } else if (imageSrc.startsWith('data:')) { mappedResources[imageSrc] = imageSrc; } else { - const filePath = `${Setting.value('tempDir')}/${md5(Date.now() + Math.random())}`; - await shim.fetchBlob(imageSrc, { path: filePath }); - const createdResource = await shim.createResourceFromPath(filePath); - await shim.fsDriver().remove(filePath); - mappedResources[imageSrc] = `file://${encodeURI(Resource.fullPath(createdResource))}`; + downloadImages.push(downloadImage(imageSrc)); } } catch (error) { - logger.warn(`Error creating a resource for ${imageSrc}.`, error); + logger.warn(`processPastedHtml: Error creating a resource for ${imageSrc}.`, error); mappedResources[imageSrc] = imageSrc; } } } + await Promise.all(downloadImages); + // TinyMCE can accept any type of HTML, including HTML that may not be preserved once saved as // Markdown. For example the content may have a dark background which would be supported by // TinyMCE, but lost once the note is saved. So here we convert the HTML to Markdown then back