2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
9 import type {TableCellNode} from './LexicalTableCellNode';
21 import {addClassNamesToElement, isHTMLElement} from '@lexical/utils';
23 $applyNodeReplacement,
24 $getNearestNodeFromDOMNode,
28 import {$isTableCellNode} from './LexicalTableCellNode';
29 import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
30 import {getTable} from './LexicalTableSelectionHelpers';
31 import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
33 commonPropertiesDifferent, deserializeCommonBlockNode,
34 setCommonBlockPropsFromElement,
35 updateElementWithCommonBlockProps
36 } from "lexical/nodes/common";
37 import {el, extractStyleMapFromElement, StyleMap} from "../../utils/dom";
38 import {getTableColumnWidths} from "../../utils/tables";
40 export type SerializedTableNode = Spread<{
42 styles: Record<string, string>,
43 }, SerializedCommonBlockNode>
46 export class TableNode extends CommonBlockNode {
47 __colWidths: string[] = [];
48 __styles: StyleMap = new Map;
50 static getType(): string {
54 static clone(node: TableNode): TableNode {
55 const newNode = new TableNode(node.__key);
56 copyCommonBlockProperties(node, newNode);
57 newNode.__colWidths = node.__colWidths;
58 newNode.__styles = new Map(node.__styles);
62 static importDOM(): DOMConversionMap | null {
64 table: (_node: Node) => ({
65 conversion: $convertTableElement,
71 static importJSON(_serializedNode: SerializedTableNode): TableNode {
72 const node = $createTableNode();
73 deserializeCommonBlockNode(_serializedNode, node);
74 node.setColWidths(_serializedNode.colWidths);
75 node.setStyles(new Map(Object.entries(_serializedNode.styles)));
79 constructor(key?: NodeKey) {
83 exportJSON(): SerializedTableNode {
85 ...super.exportJSON(),
88 colWidths: this.__colWidths,
89 styles: Object.fromEntries(this.__styles),
93 createDOM(config: EditorConfig, editor?: LexicalEditor): HTMLElement {
94 const tableElement = document.createElement('table');
96 addClassNamesToElement(tableElement, config.theme.table);
98 updateElementWithCommonBlockProps(tableElement, this);
100 const colWidths = this.getColWidths();
101 if (colWidths.length > 0) {
102 const colgroup = el('colgroup');
103 for (const width of colWidths) {
104 const col = el('col');
106 col.style.width = width;
108 colgroup.append(col);
110 tableElement.append(colgroup);
113 for (const [name, value] of this.__styles.entries()) {
114 tableElement.style.setProperty(name, value);
120 updateDOM(_prevNode: TableNode): boolean {
121 return commonPropertiesDifferent(_prevNode, this)
122 || this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')
123 || this.__styles.size !== _prevNode.__styles.size
124 || (Array.from(this.__styles.values()).join(':') !== (Array.from(_prevNode.__styles.values()).join(':')));
127 exportDOM(editor: LexicalEditor): DOMExportOutput {
129 ...super.exportDOM(editor),
130 after: (tableElement) => {
135 const newElement = tableElement.cloneNode() as ParentNode;
136 const tBody = document.createElement('tbody');
138 if (isHTMLElement(tableElement)) {
139 for (const child of Array.from(tableElement.children)) {
140 if (child.nodeName === 'TR') {
143 newElement.append(child);
148 newElement.append(tBody);
150 return newElement as HTMLElement;
155 canBeEmpty(): false {
159 isShadowRoot(): boolean {
163 setColWidths(widths: string[]) {
164 const self = this.getWritable();
165 self.__colWidths = widths;
168 getColWidths(): string[] {
169 const self = this.getLatest();
170 return self.__colWidths;
173 getStyles(): StyleMap {
174 const self = this.getLatest();
175 return new Map(self.__styles);
178 setStyles(styles: StyleMap): void {
179 const self = this.getWritable();
180 self.__styles = new Map(styles);
183 getCordsFromCellNode(
184 tableCellNode: TableCellNode,
185 table: TableDOMTable,
186 ): {x: number; y: number} {
187 const {rows, domRows} = table;
189 for (let y = 0; y < rows; y++) {
190 const row = domRows[y];
196 const x = row.findIndex((cell) => {
201 const cellNode = $getNearestNodeFromDOMNode(elem);
202 return cellNode === tableCellNode;
210 throw new Error('Cell not found in table.');
216 table: TableDOMTable,
217 ): null | TableDOMCell {
218 const {domRows} = table;
220 const row = domRows[y];
226 const index = x < row.length ? x : row.length - 1;
228 const cell = row[index];
237 getDOMCellFromCordsOrThrow(
240 table: TableDOMTable,
242 const cell = this.getDOMCellFromCords(x, y, table);
245 throw new Error('Cell not found at cords.');
251 getCellNodeFromCords(
254 table: TableDOMTable,
255 ): null | TableCellNode {
256 const cell = this.getDOMCellFromCords(x, y, table);
262 const node = $getNearestNodeFromDOMNode(cell.elem);
264 if ($isTableCellNode(node)) {
271 getCellNodeFromCordsOrThrow(
274 table: TableDOMTable,
276 const node = this.getCellNodeFromCords(x, y, table);
279 throw new Error('Node at cords not TableCellNode.');
285 canSelectBefore(): true {
294 export function $getElementForTableNode(
295 editor: LexicalEditor,
296 tableNode: TableNode,
298 const tableElement = editor.getElementByKey(tableNode.getKey());
300 if (tableElement == null) {
301 throw new Error('Table Element Not Found');
304 return getTable(tableElement);
307 export function $convertTableElement(element: HTMLElement): DOMConversionOutput {
308 const node = $createTableNode();
309 setCommonBlockPropsFromElement(element, node);
311 const colWidths = getTableColumnWidths(element as HTMLTableElement);
312 node.setColWidths(colWidths);
313 node.setStyles(extractStyleMapFromElement(element));
318 export function $createTableNode(): TableNode {
319 return $applyNodeReplacement(new TableNode());
322 export function $isTableNode(
323 node: LexicalNode | null | undefined,
324 ): node is TableNode {
325 return node instanceof TableNode;