Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug Fix: Fix ContextMenu's paste option retaining style #6776

Closed
20 changes: 16 additions & 4 deletions packages/lexical-clipboard/src/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
*
*/

import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
import {
$convertHTMLtoLexicalJSON,
$generateHtmlFromNodes,
$generateNodesFromDOM,
} from '@lexical/html';
import {$addNodeStyle, $sliceSelectedTextNodeContent} from '@lexical/selection';
import {objectKlassEquals} from '@lexical/utils';
import {
Expand Down Expand Up @@ -39,6 +43,7 @@ export interface LexicalClipboardData {
'text/html'?: string | undefined;
'application/x-lexical-editor'?: string | undefined;
'text/plain': string;
'web application/x-lexical-editor'?: string | undefined;
}

/**
Expand Down Expand Up @@ -128,12 +133,17 @@ export function $insertDataTransferForPlainText(
* @param selection the selection to use as the insertion point for the content in the DataTransfer object
* @param editor the LexicalEditor the content is being inserted into.
*/
export function $insertDataTransferForRichText(
export async function $insertDataTransferForRichText(
dataTransfer: DataTransfer,
selection: BaseSelection,
editor: LexicalEditor,
): void {
const lexicalString = dataTransfer.getData('application/x-lexical-editor');
): Promise<void> {
let lexicalString = dataTransfer.getData('application/x-lexical-editor');
//clibpoard API (used by contextmenu) does not support 'application/x-lexical-editor, meaning we need to manually create it
//TODO: a better alternative solution for this?
if (!lexicalString) {
lexicalString = await $convertHTMLtoLexicalJSON(dataTransfer, editor);
}

if (lexicalString) {
try {
Expand Down Expand Up @@ -500,13 +510,15 @@ function $copyToClipboardEvent(
if (clipboardData === null) {
return false;
}

setLexicalClipboardDataTransfer(clipboardData, data);
return true;
}

const clipboardDataFunctions = [
['text/html', $getHtmlContent],
['application/x-lexical-editor', $getLexicalContent],
['web application/x-lexical-editor', $getLexicalContent],
] as const;

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/lexical-html/flow/LexicalHtml.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ declare export function $generateNodesFromDOM(
editor: LexicalEditor,
dom: Document,
): Array<LexicalNode>;

declare export function $convertHTMLtoLexicalJSON(
dataTransfer: DataTransfer,
editor:LexicalEditor,): string;
72 changes: 72 additions & 0 deletions packages/lexical-html/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,22 @@ import type {
LexicalNode,
} from 'lexical';

import {$generateJSONFromSelectedNodes} from '@lexical/clipboard';
import {$sliceSelectedTextNodeContent} from '@lexical/selection';
import {isBlockDomNode, isHTMLElement} from '@lexical/utils';
import {
$cloneWithProperties,
$createLineBreakNode,
$createParagraphNode,
$createRangeSelection,
$getRoot,
$getSelection,
$isBlockElementNode,
$isElementNode,
$isParagraphNode,
$isRangeSelection,
$isRootOrShadowRoot,
$isTabNode,
$isTextNode,
ArtificialNode__DO_NOT_USE,
ElementNode,
Expand Down Expand Up @@ -379,3 +385,69 @@ function isDomNodeBetweenTwoInlineNodes(node: Node): boolean {
isInlineDomNode(node.nextSibling) && isInlineDomNode(node.previousSibling)
);
}

export function $convertHTMLtoLexicalJSON(
clipboardData: DataTransfer,
editor: LexicalEditor,
): Promise<string | null> {
return new Promise((resolve) => {
const html = clipboardData.getData('text/html');
if (!html) {
resolve(null);
return;
}

editor.update(() => {
const domParser = new DOMParser();
const dom = domParser.parseFromString(html, 'text/html');
const nodes = $generateNodesFromDOM(editor, dom);
const selection = $getSelection();
if (nodes.length === 0) {
resolve(null);
return;
}

const styles = getStylesFromHTML(html);
applyStylesToNodes(nodes, styles);

const rangeSelection = $isRangeSelection(selection)
? selection.clone()
: $createRangeSelection();

rangeSelection.insertNodes(nodes);
const lexicalContent = JSON.stringify(
$generateJSONFromSelectedNodes(editor, selection),
);
resolve(lexicalContent);
});
});
}

function applyStylesToNodes(nodes: LexicalNode[], styles: string[]): void {
let styleIndex = 0;

nodes.forEach((node) => {
if (
$isElementNode(node) ||
$isParagraphNode(node) ||
$isTextNode(node) ||
$isTabNode(node)
) {
const style = styles[styleIndex] || '';
node.setStyle(style);
styleIndex++;
}
});
}

function getStylesFromHTML(html: string): string[] {
const styleRegex = /style="([^"]*)"/g;
const styles: string[] = [];

let match;
while ((match = styleRegex.exec(html)) !== null) {
styles.push(match[1]);
}

return styles;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*
*/

import {selectAll} from '../../../keyboardShortcuts/index.mjs';
import {
assertHTML,
click,
Expand Down Expand Up @@ -52,4 +53,44 @@ test.describe('ContextMenuCopyAndPaste', () => {
`,
);
});
test('Rich text Copy and Paste', async ({page, isRichText}) => {
test.skip(isRichText);
await focusEditor(page);
await page.keyboard.type('MLH Fellowship');
await page.keyboard.press('Enter');
await page.keyboard.press('Enter');
await page.keyboard.type('Fall 2024');

await selectAll();
await click(page, '.font-increment');
await click(page, '.lock');

await page.pause();
await doubleClick(page, 'div[contenteditable="false"] span');
await page.pause();
await click(page, 'div[contenteditable="false"] span', {button: 'right'});
await click(page, '#typeahead-menu [role="option"] :text("Copy")');

await click(page, '.unlock');
await focusEditor(page);

await pasteFromClipboard(page);

await assertHTML(
page,
html`
<p class="PlaygroundEditorTheme__paragraph" dir="ltr">
<span style="font-size: 17px; white-space: pre-wrap;">
MLH Fellowship
</span>
</p>
<p class="PlaygroundEditorTheme__paragraph">
<br />
</p>
<p class="PlaygroundEditorTheme__paragraph" dir="ltr">
<span style="font-size: 17px; white-space: pre-wrap;">Fall 2024</span>
</p>
`,
);
});
});
Loading