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:
parent
9e9bf63d70
commit
2226b79c46
@ -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
3
.gitignore
vendored
@ -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
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
28
packages/lib/htmlUtils.test.ts
Normal file
28
packages/lib/htmlUtils.test.ts
Normal 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"/>',
|
||||||
|
'<img onerror="http://downloadmalware.com"/>',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const t of testCases) {
|
||||||
|
const [input, expected] = t;
|
||||||
|
const actual = plainTextToHtml(input);
|
||||||
|
expect(actual).toBe(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -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('');
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user