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:
parent
60c2964acd
commit
2c9bf9f03a
@ -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/openEditDialog.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/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -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/openEditDialog.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/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||
|
@ -27,6 +27,7 @@ import bridge from '../../../../services/bridge';
|
||||
import { TinyMceEditorEvents } from './utils/types';
|
||||
import type { Editor } from 'tinymce';
|
||||
import { joplinCommandToTinyMceCommands, TinyMceCommand } from './utils/joplinCommandToTinyMceCommands';
|
||||
import shouldPasteResources from './utils/shouldPasteResources';
|
||||
const { clipboard } = require('electron');
|
||||
const supportedLocales = require('./supportedLocales');
|
||||
|
||||
@ -1085,15 +1086,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
// formatted text.
|
||||
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);
|
||||
|
||||
if (shouldPasteResources(pastedText, pastedHtml, resourceMds)) {
|
||||
if (resourceMds.length) {
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
@ -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;
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import htmlUtils, { extractHtmlBody } from './htmlUtils';
|
||||
import htmlUtils, { extractHtmlBody, htmlDocIsImageOnly } from './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);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -404,4 +404,33 @@ export const extractHtmlBody = (html: string) => {
|
||||
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();
|
||||
|
Loading…
Reference in New Issue
Block a user