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:
parent
4b7f0bfbb9
commit
b1877fcd0d
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user