0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-21 23:03:11 -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 { createElement, detach } from './node/Node';
import { getStartBlockOfRange, getEndBlockOfRange } from './range/Block';
@ -9,78 +8,12 @@ import {
} from './range/Boundaries';
import type { Squire } from './Editor';
import { getTextContentsOfRange } from './range/Contents';
// ---
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 = (
event: ClipboardEvent,
range: Range,
@ -91,55 +24,95 @@ const extractRangeToClipboard = (
plainTextOnly: boolean,
): boolean => {
// Edge only seems to support setting plain text as of 2016-03-11.
if (!isLegacyEdge && event.clipboardData) {
// Clipboard content should include all parents within block, or all
// parents up to root if selection across blocks
const startBlock = getStartBlockOfRange(range, root);
const endBlock = getEndBlockOfRange(range, root);
let copyRoot = root;
// 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!
if (
startBlock === endBlock &&
startBlock?.contains(range.commonAncestorContainer)
) {
copyRoot = startBlock;
}
// Extract the contents
let contents: Node;
if (removeRangeFromDocument) {
contents = deleteContentsOfRange(range, root);
} else {
// Clone range to mutate, then move up as high as possible without
// passing the copy root node.
range = range.cloneRange();
moveRangeBoundariesDownTree(range);
moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root);
contents = range.cloneContents();
}
// Add any other parents not in extracted content, up to copy root
let parent = range.commonAncestorContainer;
if (parent instanceof Text) {
parent = parent.parentNode!;
}
while (parent && parent !== copyRoot) {
const newContents = parent.cloneNode(false);
newContents.appendChild(contents);
contents = newContents;
parent = parent.parentNode!;
}
// Set clipboard data
setClipboardData(
event,
contents,
root,
toCleanHTML,
toPlainText,
plainTextOnly,
);
return true;
const clipboardData = event.clipboardData;
if (isLegacyEdge || !clipboardData) {
return false;
}
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
// parents up to root if selection across blocks
const startBlock = getStartBlockOfRange(range, root);
const endBlock = getEndBlockOfRange(range, root);
let copyRoot = root;
// 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!
if (
startBlock === endBlock &&
startBlock?.contains(range.commonAncestorContainer)
) {
copyRoot = startBlock;
}
// Extract the contents
let contents: Node;
if (removeRangeFromDocument) {
contents = deleteContentsOfRange(range, root);
} else {
// Clone range to mutate, then move up as high as possible without
// passing the copy root node.
range = range.cloneRange();
moveRangeBoundariesDownTree(range);
moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root);
contents = range.cloneContents();
}
// Add any other parents not in extracted content, up to copy root
let parent = range.commonAncestorContainer;
if (parent instanceof Text) {
parent = parent.parentNode!;
}
while (parent && parent !== copyRoot) {
const newContents = parent.cloneNode(false);
newContents.appendChild(contents);
contents = newContents;
parent = parent.parentNode!;
}
// Get HTML version of data
let html: 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 {
const node = createElement('DIV') as HTMLDivElement;
node.appendChild(contents);
html = node.innerHTML;
if (toCleanHTML) {
html = toCleanHTML(html);
}
}
// 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;
};
// ---