Skip to content

Commit

Permalink
Desktop: Speed up pasting text and images in Rich Text Editor
Browse files Browse the repository at this point in the history
  • Loading branch information
laurent22 committed Feb 8, 2024
1 parent 4b7f0bf commit b1877fc
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 13 deletions.
17 changes: 12 additions & 5 deletions packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx
Expand Up @@ -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';
Expand All @@ -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 '<div id="rendered-md"></div>',
// it would end up as '<div id="rendered-md"><br/></div>' once rendered
// (an additional <br/> was inserted).
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -1132,23 +1134,28 @@ 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,
markupToHtml.current,
);
editor.insertContent(modifiedHtml);
} else { // Handles plain text
logger.info('onPaste: pasting as text');
pasteAsPlainText(pastedText);
}
}
Expand Down
Expand Up @@ -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;
}

Expand Down
28 changes: 21 additions & 7 deletions packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts
Expand Up @@ -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<void>[] = [];

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));
Expand All @@ -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
Expand Down

0 comments on commit b1877fc

Please sign in to comment.