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

Desktop: Fixed pasting HTML in Rich Text editor, and improved pasting plain text

This commit is contained in:
Laurent Cozic 2021-05-20 18:08:59 +02:00
parent 9e9bf63d70
commit 2226b79c46
6 changed files with 79 additions and 11 deletions

View File

@ -953,6 +953,9 @@ packages/lib/fs-driver-node.js.map
packages/lib/htmlUtils.d.ts packages/lib/htmlUtils.d.ts
packages/lib/htmlUtils.js packages/lib/htmlUtils.js
packages/lib/htmlUtils.js.map packages/lib/htmlUtils.js.map
packages/lib/htmlUtils.test.d.ts
packages/lib/htmlUtils.test.js
packages/lib/htmlUtils.test.js.map
packages/lib/import-enex-md-gen.d.ts packages/lib/import-enex-md-gen.d.ts
packages/lib/import-enex-md-gen.js packages/lib/import-enex-md-gen.js
packages/lib/import-enex-md-gen.js.map packages/lib/import-enex-md-gen.js.map

3
.gitignore vendored
View File

@ -939,6 +939,9 @@ packages/lib/fs-driver-node.js.map
packages/lib/htmlUtils.d.ts packages/lib/htmlUtils.d.ts
packages/lib/htmlUtils.js packages/lib/htmlUtils.js
packages/lib/htmlUtils.js.map packages/lib/htmlUtils.js.map
packages/lib/htmlUtils.test.d.ts
packages/lib/htmlUtils.test.js
packages/lib/htmlUtils.test.js.map
packages/lib/import-enex-md-gen.d.ts packages/lib/import-enex-md-gen.d.ts
packages/lib/import-enex-md-gen.js packages/lib/import-enex-md-gen.js
packages/lib/import-enex-md-gen.js.map packages/lib/import-enex-md-gen.js.map

View File

@ -20,6 +20,7 @@ const taboverride = require('taboverride');
import { reg } from '@joplin/lib/registry'; 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';
const { themeStyle } = require('@joplin/lib/theme'); const { themeStyle } = require('@joplin/lib/theme');
const { clipboard } = require('electron'); const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales'); const supportedLocales = require('./supportedLocales');
@ -1037,6 +1038,10 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
} }
async function onPaste(event: any) { async function onPaste(event: any) {
// We do not use the default pasting behaviour because the input has
// to be processed in various ways.
event.preventDefault();
const resourceMds = await handlePasteEvent(event); const resourceMds = await handlePasteEvent(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 }));
@ -1045,23 +1050,25 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const pastedText = event.clipboardData.getData('text/plain'); const pastedText = event.clipboardData.getData('text/plain');
if (BaseItem.isMarkdownTag(pastedText)) { // Paste a link to a note if (BaseItem.isMarkdownTag(pastedText)) { // Paste a link to a note
event.preventDefault();
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
// HACK: TinyMCE doesn't add an undo step when pasting, for unclear reasons
// so we manually add it here. We also can't do it immediately it seems, or
// else nothing is added to the stack, so do it on the next frame.
const pastedHtml = event.clipboardData.getData('text/html'); const pastedHtml = event.clipboardData.getData('text/html');
if (pastedHtml) { if (pastedHtml) { // Handles HTML
event.preventDefault();
const modifiedHtml = await processPastedHtml(pastedHtml); const modifiedHtml = await processPastedHtml(pastedHtml);
editor.insertContent(modifiedHtml); editor.insertContent(modifiedHtml);
} else { // Handles plain text
pasteAsPlainText(pastedText);
} }
window.requestAnimationFrame(() => editor.undoManager.add()); // This code before was necessary to get undo working after
onChangeHandler(); // pasting but it seems it's no longer necessary, so
// removing it for now. We also couldn't do it immediately
// it seems, or else nothing is added to the stack, so do it
// on the next frame.
//
// window.requestAnimationFrame(() =>
// editor.undoManager.add()); onChangeHandler();
} }
} }
} }
@ -1080,6 +1087,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
onChangeHandler(); onChangeHandler();
} }
function pasteAsPlainText(text: string = null) {
const pastedText = text === null ? clipboard.readText() : text;
if (pastedText) {
editor.insertContent(plainTextToHtml(pastedText));
}
}
function onKeyDown(event: any) { function onKeyDown(event: any) {
// It seems "paste as text" is handled automatically by // It seems "paste as text" is handled automatically by
// on Windows so the code below so we need to run the below // on Windows so the code below so we need to run the below
@ -1092,8 +1106,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// it here and we don't need to do anything special in onPaste // it here and we don't need to do anything special in onPaste
if (!shim.isWindows()) { if (!shim.isWindows()) {
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') { if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') {
const pastedText = clipboard.readText(); pasteAsPlainText();
if (pastedText) editor.insertContent(pastedText);
} }
} }
} }

View File

@ -135,6 +135,13 @@ export async function processPastedHtml(html: string) {
const allImageUrls: string[] = []; const allImageUrls: string[] = [];
const mappedResources: Record<string, string> = {}; const mappedResources: Record<string, string> = {};
// When copying text from eg. GitHub, the HTML might contain non-breaking
// spaces instead of regular spaces. If these non-breaking spaces are
// inserted into the TinyMCE editor (using insertContent), they will be
// dropped. So here we convert them to regular spaces.
// https://stackoverflow.com/a/31790544/561309
html = html.replace(/[\u202F\u00A0]/g, ' ');
htmlUtils.replaceImageUrls(html, (src: string) => { htmlUtils.replaceImageUrls(html, (src: string) => {
allImageUrls.push(src); allImageUrls.push(src);
}); });

View File

@ -0,0 +1,28 @@
import { plainTextToHtml } from './htmlUtils';
describe('htmlUtils', () => {
test('should convert a plain text string to its HTML equivalent', () => {
const testCases = [
[
'',
'',
],
[
'line 1\nline 2',
'<p>line 1</p><p>line 2</p>',
],
[
'<img onerror="http://downloadmalware.com"/>',
'&lt;img onerror=&quot;http://downloadmalware.com&quot;/&gt;',
],
];
for (const t of testCases) {
const [input, expected] = t;
const actual = plainTextToHtml(input);
expect(actual).toBe(expected);
}
});
});

View File

@ -2,6 +2,7 @@ const urlUtils = require('./urlUtils.js');
const Entities = require('html-entities').AllHtmlEntities; const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode; const htmlentities = new Entities().encode;
const htmlparser2 = require('@joplin/fork-htmlparser2'); const htmlparser2 = require('@joplin/fork-htmlparser2');
const { escapeHtml } = require('./string-utils.js');
// [\s\S] instead of . for multiline matching // [\s\S] instead of . for multiline matching
// https://stackoverflow.com/a/16119722/561309 // https://stackoverflow.com/a/16119722/561309
@ -153,3 +154,16 @@ class HtmlUtils {
} }
export default new HtmlUtils(); export default new HtmlUtils();
export function plainTextToHtml(plainText: string): string {
const lines = plainText
.replace(/[\n\r]/g, '\n')
.split('\n');
const lineOpenTag = lines.length > 1 ? '<p>' : '';
const lineCloseTag = lines.length > 1 ? '</p>' : '';
return lines
.map(line => lineOpenTag + escapeHtml(line) + lineCloseTag)
.join('');
}