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/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
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/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
|
||||||
|
@ -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
|
const resourceMds = await getResourcesFromPasteEvent(event);
|
||||||
// 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) {
|
if (shouldPasteResources(pastedText, pastedHtml, resourceMds)) {
|
||||||
const resourceMds = await getResourcesFromPasteEvent(event);
|
|
||||||
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);
|
||||||
|
@ -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', () => {
|
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;
|
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();
|
||||||
|
Loading…
Reference in New Issue
Block a user