Skip to content

Commit

Permalink
[lexical-html] Feature: Support copy pasting block and inline nodes p…
Browse files Browse the repository at this point in the history
…roperly (#5857)
  • Loading branch information
potatowagon authored May 3, 2024
1 parent a0bb9b0 commit 0b9ef95
Show file tree
Hide file tree
Showing 12 changed files with 545 additions and 43 deletions.
31 changes: 1 addition & 30 deletions packages/lexical-code/src/CodeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,7 @@ export class CodeNode extends ElementNode {
const td = node as HTMLTableCellElement;
const table: HTMLTableElement | null = td.closest('table');

if (isGitHubCodeCell(td)) {
return {
conversion: convertTableCellElement,
priority: 3,
};
}
if (table && isGitHubCodeTable(table)) {
if (isGitHubCodeCell(td) || (table && isGitHubCodeTable(table))) {
// Return a no-op if it's a table cell in a code table, but not a code line.
// Otherwise it'll fall back to the T
return {
Expand Down Expand Up @@ -348,13 +342,6 @@ function convertDivElement(domNode: Node): DOMConversionOutput {
};
}
return {
after: (childLexicalNodes) => {
const domParent = domNode.parentNode;
if (domParent != null && domNode !== domParent.lastChild) {
childLexicalNodes.push($createLineBreakNode());
}
return childLexicalNodes;
},
node: isCode ? $createCodeNode() : null,
};
}
Expand All @@ -367,22 +354,6 @@ function convertCodeNoop(): DOMConversionOutput {
return {node: null};
}

function convertTableCellElement(domNode: Node): DOMConversionOutput {
// domNode is a <td> since we matched it by nodeName
const cell = domNode as HTMLTableCellElement;

return {
after: (childLexicalNodes) => {
if (cell.parentNode && cell.parentNode.nextSibling) {
// Append newline between code lines
childLexicalNodes.push($createLineBreakNode());
}
return childLexicalNodes;
},
node: null,
};
}

function isCodeElement(div: HTMLElement): boolean {
return div.style.fontFamily.match('monospace') !== null;
}
Expand Down
97 changes: 93 additions & 4 deletions packages/lexical-html/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,18 @@ import {
$cloneWithProperties,
$sliceSelectedTextNodeContent,
} from '@lexical/selection';
import {isHTMLElement} from '@lexical/utils';
import {$getRoot, $isElementNode, $isTextNode} from 'lexical';
import {isBlockDomNode, isHTMLElement} from '@lexical/utils';
import {
$createLineBreakNode,
$createParagraphNode,
$getRoot,
$isBlockElementNode,
$isElementNode,
$isRootOrShadowRoot,
$isTextNode,
ArtificialNode__DO_NOT_USE,
ElementNode,
} from 'lexical';

/**
* How you parse your html string to get a document is left up to you. In the browser you can use the native
Expand All @@ -33,15 +43,22 @@ export function $generateNodesFromDOM(
): Array<LexicalNode> {
const elements = dom.body ? dom.body.childNodes : [];
let lexicalNodes: Array<LexicalNode> = [];
const allArtificialNodes: Array<ArtificialNode__DO_NOT_USE> = [];
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (!IGNORE_TAGS.has(element.nodeName)) {
const lexicalNode = $createNodesFromDOM(element, editor);
const lexicalNode = $createNodesFromDOM(
element,
editor,
allArtificialNodes,
false,
);
if (lexicalNode !== null) {
lexicalNodes = lexicalNodes.concat(lexicalNode);
}
}
}
unwrapArtificalNodes(allArtificialNodes);

return lexicalNodes;
}
Expand Down Expand Up @@ -161,7 +178,6 @@ function getConversionFunction(
if (cachedConversions !== undefined) {
for (const cachedConversion of cachedConversions) {
const domConversion = cachedConversion(domNode);

if (
domConversion !== null &&
(currentConversion === null ||
Expand All @@ -180,6 +196,8 @@ const IGNORE_TAGS = new Set(['STYLE', 'SCRIPT']);
function $createNodesFromDOM(
node: Node,
editor: LexicalEditor,
allArtificialNodes: Array<ArtificialNode__DO_NOT_USE>,
hasBlockAncestorLexicalNode: boolean,
forChildMap: Map<string, DOMChildConversion> = new Map(),
parentLexicalNode?: LexicalNode | null | undefined,
): Array<LexicalNode> {
Expand Down Expand Up @@ -234,11 +252,20 @@ function $createNodesFromDOM(
const children = node.childNodes;
let childLexicalNodes = [];

const hasBlockAncestorLexicalNodeForChildren =
currentLexicalNode != null && $isRootOrShadowRoot(currentLexicalNode)
? false
: (currentLexicalNode != null &&
$isBlockElementNode(currentLexicalNode)) ||
hasBlockAncestorLexicalNode;

for (let i = 0; i < children.length; i++) {
childLexicalNodes.push(
...$createNodesFromDOM(
children[i],
editor,
allArtificialNodes,
hasBlockAncestorLexicalNodeForChildren,
new Map(forChildMap),
currentLexicalNode,
),
Expand All @@ -249,6 +276,22 @@ function $createNodesFromDOM(
childLexicalNodes = postTransform(childLexicalNodes);
}

if (isBlockDomNode(node)) {
if (!hasBlockAncestorLexicalNodeForChildren) {
childLexicalNodes = wrapContinuousInlines(
node,
childLexicalNodes,
$createParagraphNode,
);
} else {
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, () => {
const artificialNode = new ArtificialNode__DO_NOT_USE();
allArtificialNodes.push(artificialNode);
return artificialNode;
});
}
}

if (currentLexicalNode == null) {
// If it hasn't been converted to a LexicalNode, we hoist its children
// up to the same level as it.
Expand All @@ -263,3 +306,49 @@ function $createNodesFromDOM(

return lexicalNodes;
}

function wrapContinuousInlines(
domNode: Node,
nodes: Array<LexicalNode>,
createWrapperFn: () => ElementNode,
): Array<LexicalNode> {
const out: Array<LexicalNode> = [];
let continuousInlines: Array<LexicalNode> = [];
// wrap contiguous inline child nodes in para
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if ($isBlockElementNode(node)) {
out.push(node);
} else {
continuousInlines.push(node);
if (
i === nodes.length - 1 ||
(i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1]))
) {
const wrapper = createWrapperFn();
wrapper.append(...continuousInlines);
out.push(wrapper);
continuousInlines = [];
}
}
}
return out;
}

function unwrapArtificalNodes(
allArtificialNodes: Array<ArtificialNode__DO_NOT_USE>,
) {
for (const node of allArtificialNodes) {
if (node.getNextSibling() instanceof ArtificialNode__DO_NOT_USE) {
node.insertAfter($createLineBreakNode());
}
}
// Replace artificial node with it's children
for (const node of allArtificialNodes) {
const children = node.getChildren();
for (const child of children) {
node.insertBefore(child);
}
node.remove();
}
}
8 changes: 6 additions & 2 deletions packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,11 @@ test.describe('CodeBlock', () => {
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">XDS_RICH_TEXT_AREA</span>
<span
class="PlaygroundEditorTheme__textStrikethrough"
data-lexical-text="true">
XDS_RICH_TEXT_AREA
</span>
</p>
</td>
<td class="PlaygroundEditorTheme__tableCell">
Expand Down Expand Up @@ -1120,7 +1124,7 @@ test.describe('CodeBlock', () => {
{
expectedHTML: EXPECTED_HTML_GOOGLE_SPREADSHEET,
name: 'Google Spreadsheet',
pastedHTML: `<google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none" data-sheets-root="1"><colgroup><col width="100"/><col width="210"/><col width="100"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-weight:bold;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Surface&quot;}">Surface</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-style:italic;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;MWP_WORK_LS_COMPOSER&quot;}">MWP_WORK_LS_COMPOSER</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:underline;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:77349}">77349</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Lexical&quot;}">Lexical</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;XDS_RICH_TEXT_AREA&quot;}">XDS_RICH_TEXT_AREA</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;sdvd sdfvsfs&quot;}" data-sheets-textstyleruns="{&quot;1&quot;:0}{&quot;1&quot;:5,&quot;2&quot;:{&quot;5&quot;:1}}"><span style="font-size:10pt;font-family:Arial;font-style:normal;">sdvd </span><span style="font-size:10pt;font-family:Arial;font-weight:bold;font-style:normal;">sdfvsfs</span></td></tr></tbody></table>`,
pastedHTML: `<google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none" data-sheets-root="1"><colgroup><col width="100"/><col width="189"/><col width="171"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-weight:bold;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Surface&quot;}">Surface</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-style:italic;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;MWP_WORK_LS_COMPOSER&quot;}">MWP_WORK_LS_COMPOSER</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:underline;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:77349}">77349</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Lexical&quot;}">Lexical</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:line-through;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;XDS_RICH_TEXT_AREA&quot;}">XDS_RICH_TEXT_AREA</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;sdvd sdfvsfs&quot;}" data-sheets-textstyleruns="{&quot;1&quot;:0}{&quot;1&quot;:5,&quot;2&quot;:{&quot;5&quot;:1}}"><span style="font-size:10pt;font-family:Arial;font-style:normal;">sdvd </span><span style="font-size:10pt;font-family:Arial;font-weight:bold;font-style:normal;">sdfvsfs</span></td></tr></tbody></table>`,
},
];

Expand Down
Loading

0 comments on commit 0b9ef95

Please sign in to comment.