1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Desktop: Fixed copying and pasting an image from Chrome in RTE

This commit is contained in:
Laurent Cozic 2023-11-17 16:47:05 +00:00
parent 60c2964acd
commit 2c9bf9f03a
7 changed files with 168 additions and 9 deletions

View File

@ -249,6 +249,8 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.test.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js

2
.gitignore vendored
View File

@ -231,6 +231,8 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.test.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js

View File

@ -27,6 +27,7 @@ import bridge from '../../../../services/bridge';
import { TinyMceEditorEvents } from './utils/types'; import { TinyMceEditorEvents } from './utils/types';
import type { Editor } from 'tinymce'; import type { Editor } from 'tinymce';
import { joplinCommandToTinyMceCommands, TinyMceCommand } from './utils/joplinCommandToTinyMceCommands'; import { joplinCommandToTinyMceCommands, TinyMceCommand } from './utils/joplinCommandToTinyMceCommands';
import shouldPasteResources from './utils/shouldPasteResources';
const { clipboard } = require('electron'); const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales'); const supportedLocales = require('./supportedLocales');
@ -1085,15 +1086,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// formatted text. // formatted text.
const pastedHtml = event.clipboardData.getData('text/html') ? clipboard.readHTML() : ''; const pastedHtml = event.clipboardData.getData('text/html') ? clipboard.readHTML() : '';
// We should only process the images if there is no plain text or
// HTML text in the clipboard. This is because certain applications,
// such as Word, are going to add multiple versions of the copied
// data to the clipboard - one with the text formatted as HTML, and
// one with the text as an image. In that case, we need to ignore
// the image and only process the HTML.
if (!pastedText && !pastedHtml) {
const resourceMds = await getResourcesFromPasteEvent(event); const resourceMds = await getResourcesFromPasteEvent(event);
if (shouldPasteResources(pastedText, pastedHtml, resourceMds)) {
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);

View File

@ -0,0 +1,47 @@
import shouldPasteResources from './shouldPasteResources';
describe('shouldPasteResources', () => {
test.each([
[
'',
'',
[],
true,
],
[
'some text',
'',
[],
false,
],
[
'',
'<b>some html<b>',
[],
false,
],
[
'',
'<img src="https://example.com/img.png"/>',
[],
false,
],
[
'some text',
'<img src="https://example.com/img.png"/>',
[],
false,
],
[
'',
'<img src="https://example.com/img.png"/><p>Some text</p>',
[],
false,
],
])('should tell if clipboard content should be processed as resources', (pastedText, pastedHtml, resourceMds, expected) => {
const actual = shouldPasteResources(pastedText, pastedHtml, resourceMds);
expect(actual).toBe(expected);
});
});

View File

@ -0,0 +1,49 @@
import { htmlDocIsImageOnly } from '@joplin/renderer/htmlUtils';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('shouldPasteResources');
// We should only process the images if there is no plain text or HTML text in
// the clipboard. This is because certain applications, such as Word, are going
// to add multiple versions of the copied data to the clipboard - one with the
// text formatted as HTML, and one with the text as an image. In that case, we
// need to ignore the image and only process the HTML.
//
// Additional source of troubles is that when copying an image from Chrome, the
// clipboard will contain two elements: The actual image (type=image), and an
// HTML fragment with a link to the image. Most of the time getting the image
// from the HTML will work... except if some authentication is required to
// access the image. In that case we'll end up with dead link in the RTE. For
// that reason, when there's only an image in the HTML document, we process
// instead the clipboard resources, which will contain the actual image.
//
// We have a lot of log statements so that if someone reports a bug we can ask
// them to check the console and give us the messages they have.
export default (pastedText: string, pastedHtml: string, resourceMds: string[]) => {
logger.info('Pasted text:', pastedText);
logger.info('Pasted HTML:', pastedHtml);
logger.info('Resources:', resourceMds);
if (pastedText) {
logger.info('Not pasting resources because the clipboard contains plain text');
return false;
}
if (pastedHtml) {
if (!htmlDocIsImageOnly(pastedHtml)) {
logger.info('Not pasting resources because the clipboard contains HTML, which contains more than just one image');
return false;
} else {
logger.info('Not pasting HTML because it only contains one image.');
}
if (!resourceMds.length) {
logger.info('Not pasting resources because there isn\'t any');
return false;
}
}
logger.info('Pasting resources');
return true;
};

View File

@ -1,4 +1,4 @@
import htmlUtils, { extractHtmlBody } from './htmlUtils'; import htmlUtils, { extractHtmlBody, htmlDocIsImageOnly } from './htmlUtils';
describe('htmlUtils', () => { describe('htmlUtils', () => {
@ -51,4 +51,39 @@ describe('htmlUtils', () => {
} }
}); });
test('should tell if an HTML document is an image only', () => {
const testCases: [string, boolean][] = [
[
// This is the kind of HTML that's pasted when copying an image from Chrome
'<meta charset=\'utf-8\'>\n<img src="https://example.com/img.png"/>',
true,
],
[
'',
false,
],
[
'<img src="https://example.com/img.png"/>',
true,
],
[
'<img src="https://example.com/img.png"/><img src="https://example.com/img.png"/>',
false,
],
[
'<img src="https://example.com/img.png"/><p>Some text</p>',
false,
],
[
'<img src="https://example.com/img.png"/> Some text',
false,
],
];
for (const [input, expected] of testCases) {
const actual = htmlDocIsImageOnly(input);
expect(actual).toBe(expected);
}
});
}); });

View File

@ -404,4 +404,33 @@ export const extractHtmlBody = (html: string) => {
return bodyFound ? output.join('') : html; return bodyFound ? output.join('') : html;
}; };
export const htmlDocIsImageOnly = (html: string) => {
let imageCount = 0;
let nonImageFound = false;
let textFound = false;
const parser = new htmlparser2.Parser({
onopentag: (name: string) => {
if (name === 'img') {
imageCount++;
} else if (['meta'].includes(name)) {
// We allow these tags since they don't print anything
} else {
nonImageFound = true;
}
},
ontext: (text: string) => {
if (text.trim()) textFound = true;
},
});
parser.write(html);
parser.end();
return imageCount === 1 && !nonImageFound && !textFound;
};
export default new HtmlUtils(); export default new HtmlUtils();