mirror of
https://github.com/facebook/lexical.git
synced 2025-05-17 15:18:47 +08:00
[lexical-html] Feature: Support copy pasting block and inline nodes properly (#5857)
This commit is contained in:
@ -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 {
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
@ -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;
|
||||
}
|
||||
@ -161,7 +178,6 @@ function getConversionFunction(
|
||||
if (cachedConversions !== undefined) {
|
||||
for (const cachedConversion of cachedConversions) {
|
||||
const domConversion = cachedConversion(domNode);
|
||||
|
||||
if (
|
||||
domConversion !== null &&
|
||||
(currentConversion === null ||
|
||||
@ -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> {
|
||||
@ -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,
|
||||
),
|
||||
@ -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.
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
@ -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="{"1":2,"2":"Surface"}">Surface</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-style:italic;" data-sheets-value="{"1":2,"2":"MWP_WORK_LS_COMPOSER"}">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="{"1":3,"3":77349}">77349</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"Lexical"}">Lexical</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"XDS_RICH_TEXT_AREA"}">XDS_RICH_TEXT_AREA</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"sdvd sdfvsfs"}" data-sheets-textstyleruns="{"1":0}{"1":5,"2":{"5":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="{"1":2,"2":"Surface"}">Surface</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-style:italic;" data-sheets-value="{"1":2,"2":"MWP_WORK_LS_COMPOSER"}">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="{"1":3,"3":77349}">77349</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"Lexical"}">Lexical</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:line-through;" data-sheets-value="{"1":2,"2":"XDS_RICH_TEXT_AREA"}">XDS_RICH_TEXT_AREA</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"sdvd sdfvsfs"}" data-sheets-textstyleruns="{"1":0}{"1":5,"2":{"5":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>`,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -275,4 +275,229 @@ test.describe('HTML CopyAndPaste', () => {
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
test('Copy + paste single div', async ({page, isPlainText}) => {
|
||||
test.skip(isPlainText);
|
||||
|
||||
await focusEditor(page);
|
||||
|
||||
const clipboard = {
|
||||
'text/html': `
|
||||
123
|
||||
<div>
|
||||
456
|
||||
</div>`,
|
||||
};
|
||||
|
||||
await pasteFromClipboard(page, clipboard);
|
||||
|
||||
await assertHTML(
|
||||
page,
|
||||
html`
|
||||
<p class="PlaygroundEditorTheme__paragraph">
|
||||
<span data-lexical-text="true">123</span>
|
||||
</p>
|
||||
<p class="PlaygroundEditorTheme__paragraph">
|
||||
<span data-lexical-text="true">456</span>
|
||||
</p>
|
||||
`,
|
||||
);
|
||||
await assertSelection(page, {
|
||||
anchorOffset: 3,
|
||||
anchorPath: [1, 0, 0],
|
||||
focusOffset: 3,
|
||||
focusPath: [1, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
test('Copy + paste nested divs', async ({page, isPlainText}) => {
|
||||
test.skip(isPlainText);
|
||||
|
||||
await focusEditor(page);
|
||||
|
||||
const clipboard = {
|
||||
'text/html': html`
|
||||
<div>
|
||||
a
|
||||
<div>
|
||||
b b
|
||||
<div>
|
||||
c
|
||||
<div>
|
||||
<div></div>
|
||||
z
|
||||
</div>
|
||||
</div>
|
||||
d e
|
||||
</div>
|
||||
fg
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
await pasteFromClipboard(page, clipboard);
|
||||
|
||||
await assertHTML(
|
||||
page,
|
||||
html`
|
||||
<p
|
||||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
|
||||
dir="ltr">
|
||||
<span data-lexical-text="true">a</span>
|
||||
</p>
|
||||
<p
|
||||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
|
||||
dir="ltr">
|
||||
<span data-lexical-text="true">b b</span>
|
||||
</p>
|
||||
<p
|
||||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
|
||||
dir="ltr">
|
||||
<span data-lexical-text="true">c</span>
|
||||
</p>
|
||||
<p
|
||||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
|
||||
dir="ltr">
|
||||
<span data-lexical-text="true">z</span>
|
||||
</p>
|
||||
<p
|
||||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
|
||||
dir="ltr">
|
||||
<span data-lexical-text="true">d e</span>
|
||||
</p>
|
||||
<p
|
||||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
|
||||
dir="ltr">
|
||||
<span data-lexical-text="true">fg</span>
|
||||
</p>
|
||||
`,
|
||||
);
|
||||
await assertSelection(page, {
|
||||
anchorOffset: 2,
|
||||
anchorPath: [5, 0, 0],
|
||||
focusOffset: 2,
|
||||
focusPath: [5, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
test('Copy + paste nested div in a span', async ({page, isPlainText}) => {
|
||||
test.skip(isPlainText);
|
||||
|
||||
await focusEditor(page);
|
||||
|
||||
const clipboard = {
|
||||
'text/html': html`
|
||||
<span>
|
||||
123
|
||||
<div>456</div>
|
||||
</span>
|
||||
`,
|
||||
};
|
||||
|
||||
await pasteFromClipboard(page, clipboard);
|
||||
|
||||
await assertHTML(
|
||||
page,
|
||||
html`
|
||||
<p class="PlaygroundEditorTheme__paragraph">
|
||||
<span data-lexical-text="true">123</span>
|
||||
</p>
|
||||
<p class="PlaygroundEditorTheme__paragraph">
|
||||
<span data-lexical-text="true">456</span>
|
||||
</p>
|
||||
`,
|
||||
);
|
||||
await assertSelection(page, {
|
||||
anchorOffset: 3,
|
||||
anchorPath: [1, 0, 0],
|
||||
focusOffset: 3,
|
||||
focusPath: [1, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
test('Copy + paste nested span in a div', async ({page, isPlainText}) => {
|
||||
test.skip(isPlainText);
|
||||
|
||||
await focusEditor(page);
|
||||
|
||||
const clipboard = {
|
||||
'text/html': html`
|
||||
<div>
|
||||
<span>
|
||||
123
|
||||
<div>456</div>
|
||||
</span>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
await pasteFromClipboard(page, clipboard);
|
||||
|
||||
await assertHTML(
|
||||
page,
|
||||
html`
|
||||
<p class="PlaygroundEditorTheme__paragraph">
|
||||
<span data-lexical-text="true">123</span>
|
||||
</p>
|
||||
<p class="PlaygroundEditorTheme__paragraph">
|
||||
<span data-lexical-text="true">456</span>
|
||||
</p>
|
||||
`,
|
||||
);
|
||||
await assertSelection(page, {
|
||||
anchorOffset: 3,
|
||||
anchorPath: [1, 0, 0],
|
||||
focusOffset: 3,
|
||||
focusPath: [1, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
test('Copy + paste multiple nested spans and divs', async ({
|
||||
page,
|
||||
isPlainText,
|
||||
}) => {
|
||||
test.skip(isPlainText);
|
||||
|
||||
await focusEditor(page);
|
||||
|
||||
const clipboard = {
|
||||
'text/html': html`
|
||||
<div>
|
||||
a b
|
||||
<span>
|
||||
c d
|
||||
<span>e</span>
|
||||
</span>
|
||||
<div>
|
||||
f
|
||||
<span>g h</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
await pasteFromClipboard(page, clipboard);
|
||||
|
||||
await assertHTML(
|
||||
page,
|
||||
html`
|
||||
<p
|
||||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
|
||||
dir="ltr">
|
||||
<span data-lexical-text="true">a b c d e</span>
|
||||
</p>
|
||||
<p
|
||||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
|
||||
dir="ltr">
|
||||
<span data-lexical-text="true">f g h</span>
|
||||
</p>
|
||||
`,
|
||||
);
|
||||
await assertSelection(page, {
|
||||
anchorOffset: 5,
|
||||
anchorPath: [1, 0, 0],
|
||||
focusOffset: 5,
|
||||
focusPath: [1, 0, 0],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -416,4 +416,61 @@ test.describe('HTML Lists CopyAndPaste', () => {
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
test('Copy + paste a nested divs in a list', async ({page, isPlainText}) => {
|
||||
test.skip(isPlainText);
|
||||
|
||||
await focusEditor(page);
|
||||
|
||||
const clipboard = {
|
||||
'text/html': html`
|
||||
<ol>
|
||||
<li>
|
||||
1
|
||||
<div>2</div>
|
||||
3
|
||||
</li>
|
||||
<li>
|
||||
A
|
||||
<div>B</div>
|
||||
C
|
||||
</li>
|
||||
</ol>
|
||||
`,
|
||||
};
|
||||
|
||||
await pasteFromClipboard(page, clipboard);
|
||||
|
||||
await assertHTML(
|
||||
page,
|
||||
html`
|
||||
<ol class="PlaygroundEditorTheme__ol1">
|
||||
<li value="1" class="PlaygroundEditorTheme__listItem">
|
||||
<span data-lexical-text="true">1</span>
|
||||
<br />
|
||||
<span data-lexical-text="true">2</span>
|
||||
<br />
|
||||
<span data-lexical-text="true">3</span>
|
||||
</li>
|
||||
<li
|
||||
value="2"
|
||||
class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__ltr"
|
||||
dir="ltr">
|
||||
<span data-lexical-text="true">A</span>
|
||||
<br />
|
||||
<span data-lexical-text="true">B</span>
|
||||
<br />
|
||||
<span data-lexical-text="true">C</span>
|
||||
</li>
|
||||
</ol>
|
||||
`,
|
||||
);
|
||||
|
||||
await assertSelection(page, {
|
||||
anchorOffset: 1,
|
||||
anchorPath: [0, 1, 4, 0],
|
||||
focusOffset: 1,
|
||||
focusPath: [0, 1, 4, 0],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -471,4 +471,104 @@ test.describe('HTML Tables CopyAndPaste', () => {
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
test('Copy + paste nested block and inline html in a table', async ({
|
||||
page,
|
||||
isPlainText,
|
||||
isCollab,
|
||||
}) => {
|
||||
test.skip(isPlainText);
|
||||
|
||||
test.fixme(
|
||||
isCollab,
|
||||
'Table selection styles are not properly synced to the right hand frame',
|
||||
);
|
||||
|
||||
await focusEditor(page);
|
||||
|
||||
const clipboard = {
|
||||
'text/html': html`
|
||||
123
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<span>456<span>
|
||||
</td>
|
||||
<td>
|
||||
789
|
||||
<div>
|
||||
000
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
ABC
|
||||
<div>
|
||||
000
|
||||
<div>
|
||||
000
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
DEF
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`,
|
||||
};
|
||||
|
||||
await pasteFromClipboard(page, clipboard);
|
||||
|
||||
await assertHTML(
|
||||
page,
|
||||
html`
|
||||
<p class="PlaygroundEditorTheme__paragraph">
|
||||
<span data-lexical-text="true">123</span>
|
||||
</p>
|
||||
<table class="PlaygroundEditorTheme__table">
|
||||
<tr>
|
||||
<td class="PlaygroundEditorTheme__tableCell">
|
||||
<p class="PlaygroundEditorTheme__paragraph">
|
||||
<span data-lexical-text="true">456</span>
|
||||
</p>
|
||||
</td>
|
||||
<td class="PlaygroundEditorTheme__tableCell">
|
||||
<p class="PlaygroundEditorTheme__paragraph">
|
||||
<span data-lexical-text="true">789</span>
|
||||
</p>
|
||||
<p class="PlaygroundEditorTheme__paragraph">
|
||||
<span data-lexical-text="true">000</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="PlaygroundEditorTheme__tableCell">
|
||||
<p
|
||||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
|
||||
dir="ltr">
|
||||
<span data-lexical-text="true">ABC</span>
|
||||
</p>
|
||||
<p class="PlaygroundEditorTheme__paragraph">
|
||||
<span data-lexical-text="true">000</span>
|
||||
</p>
|
||||
<p class="PlaygroundEditorTheme__paragraph">
|
||||
<span data-lexical-text="true">000</span>
|
||||
</p>
|
||||
</td>
|
||||
<td class="PlaygroundEditorTheme__tableCell">
|
||||
<p
|
||||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
|
||||
dir="ltr">
|
||||
<span data-lexical-text="true">DEF</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -44,7 +44,13 @@ import normalizeClassNames from 'shared/normalizeClassNames';
|
||||
export {default as markSelection} from './markSelection';
|
||||
export {default as mergeRegister} from './mergeRegister';
|
||||
export {default as positionNodeOnRange} from './positionNodeOnRange';
|
||||
export {$splitNode, isHTMLAnchorElement, isHTMLElement} from 'lexical';
|
||||
export {
|
||||
$splitNode,
|
||||
isBlockDomNode,
|
||||
isHTMLAnchorElement,
|
||||
isHTMLElement,
|
||||
isInlineDomNode,
|
||||
} from 'lexical';
|
||||
// Hotfix to export these with inlined types #5918
|
||||
export const CAN_USE_BEFORE_INPUT: boolean = CAN_USE_BEFORE_INPUT_;
|
||||
export const CAN_USE_DOM: boolean = CAN_USE_DOM_;
|
||||
|
@ -37,6 +37,7 @@ import {
|
||||
getDOMSelection,
|
||||
markAllNodesAsDirty,
|
||||
} from './LexicalUtils';
|
||||
import {ArtificialNode__DO_NOT_USE} from './nodes/ArtificialNode';
|
||||
import {DecoratorNode} from './nodes/LexicalDecoratorNode';
|
||||
import {LineBreakNode} from './nodes/LexicalLineBreakNode';
|
||||
import {ParagraphNode} from './nodes/LexicalParagraphNode';
|
||||
@ -421,6 +422,7 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor {
|
||||
LineBreakNode,
|
||||
TabNode,
|
||||
ParagraphNode,
|
||||
ArtificialNode__DO_NOT_USE,
|
||||
...(config.nodes || []),
|
||||
];
|
||||
const {onError, html} = config;
|
||||
|
@ -1584,6 +1584,32 @@ export function isHTMLElement(x: Node | EventTarget): x is HTMLElement {
|
||||
return x.nodeType === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node - the Dom Node to check
|
||||
* @returns if the Dom Node is an inline node
|
||||
*/
|
||||
export function isInlineDomNode(node: Node) {
|
||||
const inlineNodes = new RegExp(
|
||||
/^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var|#text)$/,
|
||||
'i',
|
||||
);
|
||||
return node.nodeName.match(inlineNodes) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node - the Dom Node to check
|
||||
* @returns if the Dom Node is a block node
|
||||
*/
|
||||
export function isBlockDomNode(node: Node) {
|
||||
const blockNodes = new RegExp(
|
||||
/^(address|article|aside|blockquote|canvas|dd|div|dl|dt|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hr|li|main|nav|noscript|ol|p|pre|section|table|td|tfoot|ul|video)$/,
|
||||
'i',
|
||||
);
|
||||
return node.nodeName.match(blockNodes) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is for internal use of the library.
|
||||
* Please do not use it as it may change in the future.
|
||||
|
@ -157,11 +157,14 @@ export {
|
||||
$setSelection,
|
||||
$splitNode,
|
||||
getNearestEditorFromDOMNode,
|
||||
isBlockDomNode,
|
||||
isHTMLAnchorElement,
|
||||
isHTMLElement,
|
||||
isInlineDomNode,
|
||||
isSelectionCapturedInDecoratorInput,
|
||||
isSelectionWithinEditor,
|
||||
} from './LexicalUtils';
|
||||
export {ArtificialNode__DO_NOT_USE} from './nodes/ArtificialNode';
|
||||
export {$isDecoratorNode, DecoratorNode} from './nodes/LexicalDecoratorNode';
|
||||
export {$isElementNode, ElementNode} from './nodes/LexicalElementNode';
|
||||
export type {SerializedLineBreakNode} from './nodes/LexicalLineBreakNode';
|
||||
|
23
packages/lexical/src/nodes/ArtificialNode.ts
Normal file
23
packages/lexical/src/nodes/ArtificialNode.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import type {EditorConfig} from 'lexical';
|
||||
|
||||
import {ElementNode} from './LexicalElementNode';
|
||||
|
||||
// TODO: Cleanup ArtificialNode__DO_NOT_USE #5966
|
||||
export class ArtificialNode__DO_NOT_USE extends ElementNode {
|
||||
static getType(): string {
|
||||
return 'artificial';
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
// this isnt supposed to be used and is not used anywhere but defining it to appease the API
|
||||
const dom = document.createElement('div');
|
||||
return dom;
|
||||
}
|
||||
}
|
@ -62,6 +62,7 @@ import {
|
||||
getCachedClassNameArray,
|
||||
internalMarkSiblingsAsDirty,
|
||||
isHTMLElement,
|
||||
isInlineDomNode,
|
||||
toggleTextFormatType,
|
||||
} from '../LexicalUtils';
|
||||
import {$createLineBreakNode} from './LexicalLineBreakNode';
|
||||
@ -1261,11 +1262,6 @@ function convertTextDOMNode(domNode: Node): DOMConversionOutput {
|
||||
return {node: $createTextNode(textContent)};
|
||||
}
|
||||
|
||||
const inlineParents = new RegExp(
|
||||
/^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/,
|
||||
'i',
|
||||
);
|
||||
|
||||
function findTextInLine(text: Text, forward: boolean): null | Text {
|
||||
let node: Node = text;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
@ -1284,7 +1280,7 @@ function findTextInLine(text: Text, forward: boolean): null | Text {
|
||||
if (node.nodeType === DOM_ELEMENT_TYPE) {
|
||||
const display = (node as HTMLElement).style.display;
|
||||
if (
|
||||
(display === '' && node.nodeName.match(inlineParents) === null) ||
|
||||
(display === '' && !isInlineDomNode(node)) ||
|
||||
(display !== '' && !display.startsWith('inline'))
|
||||
) {
|
||||
return null;
|
||||
|
Reference in New Issue
Block a user