0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 07:13:08 -05:00

Improve default plain text cut/copy

Instead of using innerText, this now uses the getTextContentsOfRange
helper fn to work out the white space, so is consistent with the
editor's getSelectedText method and should be higher fidelity.

Note, you can still override the behaviour by supplying a toPlainText
fn in the editor config (such as the one you can find at
https://github.com/fastmail/overture/blob/master/source/html/toPlainText.js)
This commit is contained in:
Neil Jenkins 2023-10-02 13:27:19 +11:00
parent 96e4bf2e98
commit 950e122c5c

View file

@ -1,4 +1,3 @@
import { cleanupBRs } from './Clean';
import { isWin, isGecko, isLegacyEdge, notWS } from './Constants'; import { isWin, isGecko, isLegacyEdge, notWS } from './Constants';
import { createElement, detach } from './node/Node'; import { createElement, detach } from './node/Node';
import { getStartBlockOfRange, getEndBlockOfRange } from './range/Block'; import { getStartBlockOfRange, getEndBlockOfRange } from './range/Block';
@ -9,78 +8,12 @@ import {
} from './range/Boundaries'; } from './range/Boundaries';
import type { Squire } from './Editor'; import type { Squire } from './Editor';
import { getTextContentsOfRange } from './range/Contents';
// --- // ---
const indexOf = Array.prototype.indexOf; const indexOf = Array.prototype.indexOf;
// The (non-standard but supported enough) innerText property is based on the
// render tree in Firefox and possibly other browsers, so we must insert the
// DOM node into the document to ensure the text part is correct.
const setClipboardData = (
event: ClipboardEvent,
contents: Node,
root: HTMLElement,
toCleanHTML: null | ((html: string) => string),
toPlainText: null | ((html: string) => string),
plainTextOnly: boolean,
): void => {
const clipboardData = event.clipboardData!;
const body = document.body;
const node = createElement('DIV') as HTMLDivElement;
let html: string | undefined;
let text: string | undefined;
if (
contents.childNodes.length === 1 &&
contents.childNodes[0] instanceof Text
) {
// Replace nbsp with regular space;
// eslint-disable-next-line no-irregular-whitespace
text = contents.childNodes[0].data.replace(/ /g, ' ');
plainTextOnly = true;
} else {
node.appendChild(contents);
html = node.innerHTML;
if (toCleanHTML) {
html = toCleanHTML(html);
}
}
if (text !== undefined) {
// Do nothing; we were copying plain text to start
} else if (toPlainText && html !== undefined) {
text = toPlainText(html);
} else {
// Firefox will add an extra new line for BRs at the end of block when
// calculating innerText, even though they don't actually affect
// display, so we need to remove them first.
cleanupBRs(node, root, true);
node.setAttribute(
'style',
'position:fixed;overflow:hidden;bottom:100%;right:100%;',
);
body.appendChild(node);
text = node.innerText || node.textContent!;
// Replace nbsp with regular space
// eslint-disable-next-line no-irregular-whitespace
text = text.replace(/ /g, ' ');
body.removeChild(node);
}
// Firefox (and others?) returns unix line endings (\n) even on Windows.
// If on Windows, normalise to \r\n, since Notepad and some other crappy
// apps do not understand just \n.
if (isWin) {
text = text.replace(/\r?\n/g, '\r\n');
}
if (!plainTextOnly && html && text !== html) {
clipboardData.setData('text/html', html);
}
clipboardData.setData('text/plain', text);
event.preventDefault();
};
const extractRangeToClipboard = ( const extractRangeToClipboard = (
event: ClipboardEvent, event: ClipboardEvent,
range: Range, range: Range,
@ -91,12 +24,20 @@ const extractRangeToClipboard = (
plainTextOnly: boolean, plainTextOnly: boolean,
): boolean => { ): boolean => {
// Edge only seems to support setting plain text as of 2016-03-11. // Edge only seems to support setting plain text as of 2016-03-11.
if (!isLegacyEdge && event.clipboardData) { const clipboardData = event.clipboardData;
if (isLegacyEdge || !clipboardData) {
return false;
}
// First get the plain text version from the range (unless we have a custom
// HTML -> Text conversion fn)
let text = toPlainText ? '' : getTextContentsOfRange(range);
// Clipboard content should include all parents within block, or all // Clipboard content should include all parents within block, or all
// parents up to root if selection across blocks // parents up to root if selection across blocks
const startBlock = getStartBlockOfRange(range, root); const startBlock = getStartBlockOfRange(range, root);
const endBlock = getEndBlockOfRange(range, root); const endBlock = getEndBlockOfRange(range, root);
let copyRoot = root; let copyRoot = root;
// If the content is not in well-formed blocks, the start and end block // If the content is not in well-formed blocks, the start and end block
// may be the same, but actually the range goes outside it. Must check! // may be the same, but actually the range goes outside it. Must check!
if ( if (
@ -105,6 +46,7 @@ const extractRangeToClipboard = (
) { ) {
copyRoot = startBlock; copyRoot = startBlock;
} }
// Extract the contents // Extract the contents
let contents: Node; let contents: Node;
if (removeRangeFromDocument) { if (removeRangeFromDocument) {
@ -117,6 +59,7 @@ const extractRangeToClipboard = (
moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root); moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root);
contents = range.cloneContents(); contents = range.cloneContents();
} }
// Add any other parents not in extracted content, up to copy root // Add any other parents not in extracted content, up to copy root
let parent = range.commonAncestorContainer; let parent = range.commonAncestorContainer;
if (parent instanceof Text) { if (parent instanceof Text) {
@ -128,18 +71,48 @@ const extractRangeToClipboard = (
contents = newContents; contents = newContents;
parent = parent.parentNode!; parent = parent.parentNode!;
} }
// Set clipboard data
setClipboardData( // Get HTML version of data
event, let html: string | undefined;
contents, if (
root, contents.childNodes.length === 1 &&
toCleanHTML, contents.childNodes[0] instanceof Text
toPlainText, ) {
plainTextOnly, // Replace nbsp with regular space;
); // eslint-disable-next-line no-irregular-whitespace
return true; text = contents.childNodes[0].data.replace(/ /g, ' ');
plainTextOnly = true;
} else {
const node = createElement('DIV') as HTMLDivElement;
node.appendChild(contents);
html = node.innerHTML;
if (toCleanHTML) {
html = toCleanHTML(html);
} }
return false; }
// Get Text version of data
if (plainTextOnly) {
// Do nothing; we were copying plain text to start
} else if (toPlainText && html !== undefined) {
text = toPlainText(html);
}
// Firefox (and others?) returns unix line endings (\n) even on Windows.
// If on Windows, normalise to \r\n, since Notepad and some other crappy
// apps do not understand just \n.
if (isWin) {
text = text.replace(/\r?\n/g, '\r\n');
}
// Set clipboard data
if (!plainTextOnly && html && text !== html) {
clipboardData.setData('text/html', html);
}
clipboardData.setData('text/plain', text);
event.preventDefault();
return true;
}; };
// --- // ---