Skip to content

Commit

Permalink
[lexical-table] Bug Fix: TableNode exportDOM fixes for partial table …
Browse files Browse the repository at this point in the history
…selection (#6889)
  • Loading branch information
etrepum authored Dec 1, 2024
1 parent 230dcf2 commit 6a1cf18
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 8 deletions.
7 changes: 6 additions & 1 deletion packages/lexical-table/src/LexicalTableCellNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
$isLineBreakNode,
$isTextNode,
ElementNode,
isHTMLElement,
} from 'lexical';

import {COLUMN_WIDTH, PIXEL_VALUE_REG_EXP} from './constants';
Expand Down Expand Up @@ -150,8 +151,12 @@ export class TableCellNode extends ElementNode {
exportDOM(editor: LexicalEditor): DOMExportOutput {
const output = super.exportDOM(editor);

if (output.element) {
if (output.element && isHTMLElement(output.element)) {
const element = output.element as HTMLTableCellElement;
element.setAttribute(
'data-temporary-table-cell-lexical-key',
this.getKey(),
);
element.style.border = '1px solid black';
if (this.__colSpan > 1) {
element.colSpan = this.__colSpan;
Expand Down
75 changes: 69 additions & 6 deletions packages/lexical-table/src/LexicalTableNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
*
*/

import type {TableRowNode} from './LexicalTableRowNode';

import {
addClassNamesToElement,
isHTMLElement,
Expand All @@ -15,6 +17,7 @@ import {
$applyNodeReplacement,
$getEditor,
$getNearestNodeFromDOMNode,
BaseSelection,
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
Expand All @@ -31,13 +34,13 @@ import {
import invariant from 'shared/invariant';

import {PIXEL_VALUE_REG_EXP} from './constants';
import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode';
import {$isTableCellNode, type TableCellNode} from './LexicalTableCellNode';
import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
import {TableRowNode} from './LexicalTableRowNode';
import {
$getNearestTableCellInTableFromDOMNode,
getTable,
} from './LexicalTableSelectionHelpers';
import {$computeTableMapSkipCellCheck} from './LexicalTableUtils';

export type SerializedTableNode = Spread<
{
Expand Down Expand Up @@ -170,6 +173,14 @@ export class TableNode extends ElementNode {
};
}

extractWithChild(
child: LexicalNode,
selection: BaseSelection | null,
destination: 'clone' | 'html',
): boolean {
return destination === 'html';
}

getDOMSlot(element: HTMLElement): ElementDOMSlot {
const tableElement =
(element.nodeName !== 'TABLE' && element.querySelector('table')) ||
Expand Down Expand Up @@ -227,11 +238,12 @@ export class TableNode extends ElementNode {
}

exportDOM(editor: LexicalEditor): DOMExportOutput {
const {element, after} = super.exportDOM(editor);
const superExport = super.exportDOM(editor);
const {element} = superExport;
return {
after: (tableElement) => {
if (after) {
tableElement = after(tableElement);
if (superExport.after) {
tableElement = superExport.after(tableElement);
}
if (
tableElement &&
Expand All @@ -243,11 +255,62 @@ export class TableNode extends ElementNode {
if (!tableElement || !isHTMLElement(tableElement)) {
return null;
}

// Scan the table map to build a map of table cell key to the columns it needs
const [tableMap] = $computeTableMapSkipCellCheck(this, null, null);
const cellValues = new Map<
NodeKey,
{startColumn: number; colSpan: number}
>();
for (const mapRow of tableMap) {
for (const mapValue of mapRow) {
const key = mapValue.cell.getKey();
if (!cellValues.has(key)) {
cellValues.set(key, {
colSpan: mapValue.cell.getColSpan(),
startColumn: mapValue.startColumn,
});
}
}
}

// scan the DOM to find the table cell keys that were used and mark those columns
const knownColumns = new Set<number>();
for (const cellDOM of tableElement.querySelectorAll(
':scope > tr > [data-temporary-table-cell-lexical-key]',
)) {
const key = cellDOM.getAttribute(
'data-temporary-table-cell-lexical-key',
);
if (key) {
const cellSpan = cellValues.get(key);
cellDOM.removeAttribute('data-temporary-table-cell-lexical-key');
if (cellSpan) {
cellValues.delete(key);
for (let i = 0; i < cellSpan.colSpan; i++) {
knownColumns.add(i + cellSpan.startColumn);
}
}
}
}

// Compute the colgroup and columns in the export
const colGroup = tableElement.querySelector(':scope > colgroup');
if (colGroup) {
// Only include the <col /> for rows that are in the output
const cols = Array.from(
tableElement.querySelectorAll(':scope > colgroup > col'),
).filter((dom, i) => knownColumns.has(i));
colGroup.replaceChildren(...cols);
}

// Wrap direct descendant rows in a tbody for export
const rows = tableElement.querySelectorAll(':scope > tr');
if (rows.length > 0) {
const tBody = document.createElement('tbody');
tBody.append(...rows);
for (const row of rows) {
tBody.appendChild(row);
}
tableElement.append(tBody);
}
return tableElement;
Expand Down
10 changes: 9 additions & 1 deletion packages/lexical-table/src/LexicalTableRowNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*
*/

import type {Spread} from 'lexical';
import type {BaseSelection, Spread} from 'lexical';

import {addClassNamesToElement} from '@lexical/utils';
import {
Expand Down Expand Up @@ -81,6 +81,14 @@ export class TableRowNode extends ElementNode {
return element;
}

extractWithChild(
child: LexicalNode,
selection: BaseSelection | null,
destination: 'clone' | 'html',
): boolean {
return destination === 'html';
}

isShadowRoot(): boolean {
return true;
}
Expand Down
130 changes: 130 additions & 0 deletions packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
*/

import {$insertDataTransferForRichText} from '@lexical/clipboard';
import {$generateHtmlFromNodes} from '@lexical/html';
import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
import {
$createTableNode,
$createTableNodeWithDimensions,
$createTableSelection,
$insertTableColumn__EXPERIMENTAL,
$isTableCellNode,
} from '@lexical/table';
import {$dfs} from '@lexical/utils';
import {
$createParagraphNode,
$createTextNode,
Expand Down Expand Up @@ -136,6 +139,133 @@ describe('LexicalTableNode tests', () => {
});
});

test('TableNode.exportDOM() with range selection', async () => {
const {editor} = testEnv;

await editor.update(() => {
const tableNode = $createTableNodeWithDimensions(
2,
2,
).setColWidths([100, 200]);
tableNode
.getAllTextNodes()
.forEach((node, i) => node.setTextContent(String(i)));
$getRoot().clear().append(tableNode);
expectHtmlToBeEqual(
$generateHtmlFromNodes(editor, $getRoot().select(0)),
html`
<table class="${editorConfig.theme.table}">
<colgroup>
<col style="width: 100px" />
<col style="width: 200px" />
</colgroup>
<tbody>
<tr>
<th
style="
border: 1px solid black;
width: 75px;
vertical-align: top;
text-align: start;
background-color: rgb(242, 243, 245);
">
<p><span style="white-space: pre-wrap">0</span></p>
</th>
<th
style="
border: 1px solid black;
width: 75px;
vertical-align: top;
text-align: start;
background-color: rgb(242, 243, 245);
">
<p><span style="white-space: pre-wrap">1</span></p>
</th>
</tr>
<tr>
<th
style="
border: 1px solid black;
width: 75px;
vertical-align: top;
text-align: start;
background-color: rgb(242, 243, 245);
">
<p><span style="white-space: pre-wrap">2</span></p>
</th>
<td
style="
border: 1px solid black;
width: 75px;
vertical-align: top;
text-align: start;
">
<p><span style="white-space: pre-wrap">3</span></p>
</td>
</tr>
</tbody>
</table>
`,
);
});
});

test('TableNode.exportDOM() with partial table selection', async () => {
const {editor} = testEnv;

await editor.update(() => {
const tableNode = $createTableNodeWithDimensions(
2,
2,
).setColWidths([100, 200]);
tableNode
.getAllTextNodes()
.forEach((node, i) => node.setTextContent(String(i)));
$getRoot().append(tableNode);
const tableSelection = $createTableSelection();
tableSelection.tableKey = tableNode.getKey();
const cells = $dfs(tableNode).flatMap(({node}) =>
$isTableCellNode(node) ? [node] : [],
);
// second column
tableSelection.anchor.set(cells[1].getKey(), 0, 'element');
tableSelection.focus.set(cells[3].getKey(), 0, 'element');
expectHtmlToBeEqual(
$generateHtmlFromNodes(editor, tableSelection),
html`
<table class="${editorConfig.theme.table}">
<colgroup><col style="width: 200px" /></colgroup>
<tbody>
<tr>
<th
style="
border: 1px solid black;
width: 75px;
vertical-align: top;
text-align: start;
background-color: rgb(242, 243, 245);
">
<p><span style="white-space: pre-wrap">1</span></p>
</th>
</tr>
<tr>
<td
style="
border: 1px solid black;
width: 75px;
vertical-align: top;
text-align: start;
">
<p><span style="white-space: pre-wrap">3</span></p>
</td>
</tr>
</tbody>
</table>
`,
);
});
});

test('Copy table from an external source', async () => {
const {editor} = testEnv;

Expand Down

0 comments on commit 6a1cf18

Please sign in to comment.