2023-05-23 22:00:05 +00:00
|
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2023-05-31 18:10:02 +00:00
|
|
|
const QUILL_EMBED_GUARD = '\uFEFF';
|
2023-05-23 22:00:05 +00:00
|
|
|
|
2023-05-31 18:10:02 +00:00
|
|
|
export function createEventHandler({
|
|
|
|
deleteSelection,
|
|
|
|
}: {
|
|
|
|
deleteSelection: boolean;
|
|
|
|
}) {
|
|
|
|
return (event: ClipboardEvent): void => {
|
|
|
|
if (!event.clipboardData) {
|
|
|
|
return;
|
|
|
|
}
|
2023-05-23 22:00:05 +00:00
|
|
|
|
2023-05-31 18:10:02 +00:00
|
|
|
const selection = window.getSelection();
|
|
|
|
if (!selection) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create synthetic html with the full selection we can put into clipboard
|
|
|
|
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());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note: we can't leave text/plain alone and just add text/signal; if we update
|
|
|
|
// clipboardData at all, all other data is reset.
|
|
|
|
const plaintext = getStringFromNode(container);
|
|
|
|
event.clipboardData?.setData('text/plain', plaintext);
|
2023-05-23 22:00:05 +00:00
|
|
|
|
2023-05-31 18:10:02 +00:00
|
|
|
event.clipboardData?.setData('text/signal', container.innerHTML);
|
2023-05-23 22:00:05 +00:00
|
|
|
|
2023-05-31 18:10:02 +00:00
|
|
|
if (deleteSelection) {
|
|
|
|
selection.deleteFromDocument();
|
|
|
|
}
|
2023-05-23 22:00:05 +00:00
|
|
|
|
2023-05-31 18:10:02 +00:00
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
};
|
2023-05-23 22:00:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function getStringFromNode(node: Node): string {
|
|
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
2023-05-31 18:10:02 +00:00
|
|
|
if (node.textContent === QUILL_EMBED_GUARD) {
|
|
|
|
return '';
|
|
|
|
}
|
2023-05-23 22:00:05 +00:00
|
|
|
return node.textContent || '';
|
|
|
|
}
|
|
|
|
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
const element = node as Element;
|
2023-05-31 18:10:02 +00:00
|
|
|
if (
|
|
|
|
element.nodeName === 'IMG' &&
|
|
|
|
(element.classList.contains('emoji') ||
|
|
|
|
element.classList.contains('emoji-blot'))
|
|
|
|
) {
|
2023-05-23 22:00:05 +00:00
|
|
|
return element.ariaLabel || '';
|
|
|
|
}
|
|
|
|
if (element.nodeName === 'BR') {
|
|
|
|
return '\n';
|
|
|
|
}
|
|
|
|
if (element.childNodes.length === 0) {
|
|
|
|
return element.textContent || '';
|
|
|
|
}
|
|
|
|
let result = '';
|
|
|
|
for (const child of element.childNodes) {
|
|
|
|
result += getStringFromNode(child);
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
element.nodeName === 'P' ||
|
|
|
|
element.nodeName === 'DIV' ||
|
|
|
|
element.nodeName === 'TIME'
|
|
|
|
) {
|
|
|
|
if (result.length > 0 && !result.endsWith('\n\n')) {
|
|
|
|
result += '\n';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|