1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-20 18:48:28 +02:00

Desktop: Speed up pasting text and images in Rich Text Editor

This commit is contained in:
Laurent Cozic 2024-02-08 12:51:31 +00:00
parent 4b7f0bfbb9
commit b1877fcd0d
3 changed files with 34 additions and 13 deletions

View File

@ -15,7 +15,6 @@ import useContextMenu from './utils/useContextMenu';
import { copyHtmlToClipboard } from '../../utils/clipboardUtils'; import { copyHtmlToClipboard } from '../../utils/clipboardUtils';
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer'; import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
import { reg } from '@joplin/lib/registry';
import BaseItem from '@joplin/lib/models/BaseItem'; import BaseItem from '@joplin/lib/models/BaseItem';
import setupToolbarButtons from './utils/setupToolbarButtons'; import setupToolbarButtons from './utils/setupToolbarButtons';
import { plainTextToHtml } from '@joplin/lib/htmlUtils'; 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 { Options as NoteStyleOptions } from '@joplin/renderer/noteStyle';
import markupRenderOptions from '../../utils/markupRenderOptions'; import markupRenderOptions from '../../utils/markupRenderOptions';
import { DropHandler } from '../../utils/useDropHandler'; import { DropHandler } from '../../utils/useDropHandler';
import Logger from '@joplin/utils/Logger';
const md5 = require('md5'); const md5 = require('md5');
const { clipboard } = require('electron'); const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales'); const supportedLocales = require('./supportedLocales');
const logger = Logger.create('TinyMCE');
// In TinyMCE 5.2, when setting the body to '<div id="rendered-md"></div>', // 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 // it would end up as '<div id="rendered-md"><br/></div>' once rendered
// (an additional <br/> was inserted). // (an additional <br/> was inserted).
@ -153,7 +155,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
if (anchor) { if (anchor) {
anchor.scrollIntoView(); anchor.scrollIntoView();
} else { } else {
reg.logger().warn('TinyMce: could not find anchor with ID ', anchorName); logger.warn('could not find anchor with ID ', anchorName);
} }
} else { } else {
props.onMessage({ channel: href }); props.onMessage({ channel: href });
@ -193,7 +195,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
execCommand: async (cmd: EditorCommand) => { execCommand: async (cmd: EditorCommand) => {
if (!editor) return false; if (!editor) return false;
reg.logger().debug('TinyMce: execCommand', cmd); logger.debug('execCommand', cmd);
let commandProcessed = true; let commandProcessed = true;
@ -215,7 +217,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
} else if (cmd.value.type === 'files') { } else if (cmd.value.type === 'files') {
insertResourcesIntoContentRef.current(cmd.value.paths, { createFileURL: !!cmd.value.createFileURL }); insertResourcesIntoContentRef.current(cmd.value.paths, { createFileURL: !!cmd.value.createFileURL });
} else { } else {
reg.logger().warn('TinyMCE: unsupported drop item: ', cmd); logger.warn('unsupported drop item: ', cmd);
} }
} else { } else {
commandProcessed = false; commandProcessed = false;
@ -249,7 +251,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
} }
if (!joplinCommandToTinyMceCommands[cmd.name]) { if (!joplinCommandToTinyMceCommands[cmd.name]) {
reg.logger().warn('TinyMCE: unsupported Joplin command: ', cmd); logger.warn('unsupported Joplin command: ', cmd);
return false; return false;
} }
@ -1132,16 +1134,20 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const resourceMds = await getResourcesFromPasteEvent(event); const resourceMds = await getResourcesFromPasteEvent(event);
if (shouldPasteResources(pastedText, pastedHtml, resourceMds)) { if (shouldPasteResources(pastedText, pastedHtml, resourceMds)) {
logger.info(`onPaste: pasting ${resourceMds.length} resources`);
if (resourceMds.length) { if (resourceMds.length) {
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true })); const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true }));
editor.insertContent(result.html); editor.insertContent(result.html);
} }
} else { } else {
if (BaseItem.isMarkdownTag(pastedText)) { // Paste a link to a note 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 })); const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, pastedText, markupRenderOptions({ bodyOnly: true }));
editor.insertContent(result.html); editor.insertContent(result.html);
} else { // Paste regular text } else { // Paste regular text
if (pastedHtml) { // Handles HTML if (pastedHtml) { // Handles HTML
logger.info('onPaste: pasting as HTML');
const modifiedHtml = await processPastedHtml( const modifiedHtml = await processPastedHtml(
pastedHtml, pastedHtml,
prop_htmlToMarkdownRef.current, prop_htmlToMarkdownRef.current,
@ -1149,6 +1155,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
); );
editor.insertContent(modifiedHtml); editor.insertContent(modifiedHtml);
} else { // Handles plain text } else { // Handles plain text
logger.info('onPaste: pasting as text');
pasteAsPlainText(pastedText); pasteAsPlainText(pastedText);
} }
} }

View File

@ -25,7 +25,7 @@ export default (pastedText: string, pastedHtml: string, resourceMds: string[]) =
logger.info('Resources:', resourceMds); logger.info('Resources:', resourceMds);
if (pastedText) { 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; return false;
} }

View File

@ -152,8 +152,24 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa
allImageUrls.push(src); 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) { for (const imageSrc of allImageUrls) {
if (!mappedResources[imageSrc]) { if (!mappedResources[imageSrc]) {
logger.info(`processPastedHtml: Processing image ${imageSrc}`);
try { try {
if (imageSrc.startsWith('file')) { if (imageSrc.startsWith('file')) {
const imageFilePath = path.normalize(fileUriToPath(imageSrc)); const imageFilePath = path.normalize(fileUriToPath(imageSrc));
@ -165,22 +181,20 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa
const createdResource = await shim.createResourceFromPath(imageFilePath); const createdResource = await shim.createResourceFromPath(imageFilePath);
mappedResources[imageSrc] = `file://${encodeURI(Resource.fullPath(createdResource))}`; mappedResources[imageSrc] = `file://${encodeURI(Resource.fullPath(createdResource))}`;
} }
} else if (imageSrc.startsWith('data:')) { // Data URIs } else if (imageSrc.startsWith('data:')) {
mappedResources[imageSrc] = imageSrc; mappedResources[imageSrc] = imageSrc;
} else { } else {
const filePath = `${Setting.value('tempDir')}/${md5(Date.now() + Math.random())}`; downloadImages.push(downloadImage(imageSrc));
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) { } catch (error) {
logger.warn(`Error creating a resource for ${imageSrc}.`, error); logger.warn(`processPastedHtml: Error creating a resource for ${imageSrc}.`, error);
mappedResources[imageSrc] = imageSrc; mappedResources[imageSrc] = imageSrc;
} }
} }
} }
await Promise.all(downloadImages);
// TinyMCE can accept any type of HTML, including HTML that may not be preserved once saved as // 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 // 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 // TinyMCE, but lost once the note is saved. So here we convert the HTML to Markdown then back