From 950e122c5c77d16ec3ca69ef907139f55726da4a Mon Sep 17 00:00:00 2001 From: Neil Jenkins Date: Mon, 2 Oct 2023 13:27:19 +1100 Subject: [PATCH] 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) --- source/Clipboard.ts | 205 +++++++++++++++++++------------------------- 1 file changed, 89 insertions(+), 116 deletions(-) diff --git a/source/Clipboard.ts b/source/Clipboard.ts index bba6b55..5675bdd 100644 --- a/source/Clipboard.ts +++ b/source/Clipboard.ts @@ -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; }; // ---