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:
parent
96e4bf2e98
commit
950e122c5c
1 changed files with 89 additions and 116 deletions
|
@ -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;
|
||||
};
|
||||
|
||||
// ---
|
||||
|
|
Loading…
Reference in a new issue