Fix copy/paste of a single-line of formatting text
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
166c192740
commit
9f5c752eef
1 changed files with 123 additions and 1 deletions
|
@ -35,7 +35,7 @@ export function createEventHandler({
|
|||
const container = document.createElement('div');
|
||||
for (let i = 0, max = selection.rangeCount; i < max; i += 1) {
|
||||
const range = selection.getRangeAt(i);
|
||||
container.appendChild(range.cloneContents());
|
||||
container.appendChild(getRangeWithContainer(range));
|
||||
}
|
||||
|
||||
// We fail over to selection.toString() because we can't pull values from the DOM if
|
||||
|
@ -111,3 +111,125 @@ function getStringFromNode(
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const CONTAINER_CLASSES = ['ql-editor', 'module-message__text'];
|
||||
const BOLD_TAG = 'strong';
|
||||
const ITALIC_TAG = 'em';
|
||||
const STRIKETHROUGH_TAG = 's';
|
||||
const MONOSPACE_CLASSES = [
|
||||
'quill--monospace',
|
||||
'MessageTextRenderer__formatting--monospace',
|
||||
];
|
||||
const SPOILER_CLASSES = [
|
||||
'quill--spoiler',
|
||||
'MessageTextRenderer__formatting--spoiler',
|
||||
'MessageTextRenderer__formatting--spoiler--revealed',
|
||||
];
|
||||
|
||||
// When the user cuts/copies single-line text which don't cross any mentions/emojo or
|
||||
// formatting boundaries, we don't get the surrounding formatting nodes in our selection.
|
||||
// So, we need to walk the DOM and re-create those containing nodes.
|
||||
function getRangeWithContainer(range: Range): Node {
|
||||
const fragment = range.cloneContents();
|
||||
|
||||
// We're talking about HTML that might look like this, from the composer:
|
||||
|
||||
// <div class="ql-editor ql-editor--loaded" contenteditable="plaintext-only" ... >
|
||||
// <div dir="auto">
|
||||
// <strong>
|
||||
// <em>
|
||||
// <s>
|
||||
// <span class="quill--spoiler">
|
||||
// <span class="quill--monospace">
|
||||
// All formatting, with no formatting boundaries, mentions or emoji.
|
||||
// </span>
|
||||
// </span>
|
||||
// </s>
|
||||
// </em>
|
||||
// </strong>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// Or like this, from a message bubble:
|
||||
|
||||
// <div class="module-message__text module-message__text--outgoing">
|
||||
// <span>
|
||||
// <span class="MessageTextRenderer__formatting--spoiler--revealed">
|
||||
// <span class="MessageTextRenderer__formatting--monospace">
|
||||
// <s>
|
||||
// <em>
|
||||
// <strong>
|
||||
// All formatting, with no formatting boundaries, mentions or emoji.
|
||||
// </strong>
|
||||
// </em>
|
||||
// </s>
|
||||
// </span>
|
||||
// </span>
|
||||
// </span>
|
||||
// <span style="display: inline-block; width: 75.7656px;"></span>
|
||||
// </div>
|
||||
|
||||
// If the range spans multiple elements, we don't have to worry about this
|
||||
const { startContainer, endContainer } = range;
|
||||
if (startContainer !== endContainer) {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
let currentNode: Element | null;
|
||||
if (startContainer.nodeType !== Node.TEXT_NODE) {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
if (fragment.childNodes.length > 1) {
|
||||
return fragment;
|
||||
}
|
||||
let finalNode = fragment.childNodes.item(0);
|
||||
if (!finalNode) {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
currentNode = startContainer.parentElement as HTMLElement;
|
||||
while (
|
||||
currentNode &&
|
||||
// eslint-disable-next-line no-loop-func
|
||||
CONTAINER_CLASSES.every(item => !currentNode?.classList.contains(item))
|
||||
) {
|
||||
const tagName = currentNode.tagName.toLowerCase();
|
||||
if (tagName === BOLD_TAG) {
|
||||
const newNode = document.createElement(BOLD_TAG);
|
||||
newNode.appendChild(finalNode);
|
||||
finalNode = newNode;
|
||||
} else if (tagName === ITALIC_TAG) {
|
||||
const newNode = document.createElement(ITALIC_TAG);
|
||||
newNode.appendChild(finalNode);
|
||||
finalNode = newNode;
|
||||
} else if (tagName === STRIKETHROUGH_TAG) {
|
||||
const newNode = document.createElement(STRIKETHROUGH_TAG);
|
||||
newNode.appendChild(finalNode);
|
||||
finalNode = newNode;
|
||||
} else if (
|
||||
// eslint-disable-next-line no-loop-func
|
||||
MONOSPACE_CLASSES.some(item => currentNode?.classList.contains(item))
|
||||
) {
|
||||
const newNode = document.createElement('span');
|
||||
// Matchers check for all classes, so we just add the first
|
||||
newNode.classList.add(MONOSPACE_CLASSES[0]);
|
||||
newNode.appendChild(finalNode);
|
||||
finalNode = newNode;
|
||||
} else if (
|
||||
// eslint-disable-next-line no-loop-func
|
||||
SPOILER_CLASSES.some(item => currentNode?.classList.contains(item))
|
||||
) {
|
||||
const newNode = document.createElement('span');
|
||||
// Matchers check for all classes, so we just add the first
|
||||
newNode.classList.add(SPOILER_CLASSES[0]);
|
||||
newNode.appendChild(finalNode);
|
||||
finalNode = newNode;
|
||||
}
|
||||
|
||||
currentNode = currentNode.parentElement;
|
||||
}
|
||||
|
||||
fragment.replaceChildren(finalNode);
|
||||
return fragment;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue