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 applyCommonPropertyChanges,
34 commonPropertiesDifferent, deserializeCommonBlockNode,
35 setCommonBlockPropsFromElement,
36 updateElementWithCommonBlockProps
37 } from "lexical/nodes/common";
38 import {el, extractStyleMapFromElement, StyleMap} from "../../utils/dom";
39 import {buildColgroupFromTableWidths, getTableColumnWidths} from "../../utils/tables";
41 export type SerializedTableNode = Spread<{
43 styles: Record<string, string>,
44 }, SerializedCommonBlockNode>
47 export class TableNode extends CommonBlockNode {
48 __colWidths: string[] = [];
49 __styles: StyleMap = new Map;
51 static getType(): string {
55 static clone(node: TableNode): TableNode {
56 const newNode = new TableNode(node.__key);
57 copyCommonBlockProperties(node, newNode);
58 newNode.__colWidths = [...node.__colWidths];
59 newNode.__styles = new Map(node.__styles);
63 static importDOM(): DOMConversionMap | null {
65 table: (_node: Node) => ({
66 conversion: $convertTableElement,
72 static importJSON(_serializedNode: SerializedTableNode): TableNode {
73 const node = $createTableNode();
74 deserializeCommonBlockNode(_serializedNode, node);
75 node.setColWidths(_serializedNode.colWidths);
76 node.setStyles(new Map(Object.entries(_serializedNode.styles)));
80 constructor(key?: NodeKey) {
84 exportJSON(): SerializedTableNode {
86 ...super.exportJSON(),
89 colWidths: this.__colWidths,
90 styles: Object.fromEntries(this.__styles),
94 createDOM(config: EditorConfig, editor?: LexicalEditor): HTMLElement {
95 const tableElement = document.createElement('table');
97 addClassNamesToElement(tableElement, config.theme.table);
99 updateElementWithCommonBlockProps(tableElement, this);
101 const colWidths = this.getColWidths();
102 const colgroup = buildColgroupFromTableWidths(colWidths);
104 tableElement.append(colgroup);
107 for (const [name, value] of this.__styles.entries()) {
108 tableElement.style.setProperty(name, value);
114 updateDOM(_prevNode: TableNode, dom: HTMLElement): boolean {
115 applyCommonPropertyChanges(_prevNode, this, dom);
117 if (this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')) {
118 const existingColGroup = Array.from(dom.children).find(child => child.nodeName === 'COLGROUP');
119 const newColGroup = buildColgroupFromTableWidths(this.__colWidths);
120 if (existingColGroup) {
121 existingColGroup.remove();
125 dom.prepend(newColGroup);
129 if (Array.from(this.__styles.values()).join(':') !== Array.from(_prevNode.__styles.values()).join(':')) {
130 dom.style.cssText = '';
131 for (const [name, value] of this.__styles.entries()) {
132 dom.style.setProperty(name, value);
139 exportDOM(editor: LexicalEditor): DOMExportOutput {
141 ...super.exportDOM(editor),
142 after: (tableElement) => {
147 const newElement = tableElement.cloneNode() as ParentNode;
148 const tBody = document.createElement('tbody');
150 if (isHTMLElement(tableElement)) {
151 for (const child of Array.from(tableElement.children)) {
152 if (child.nodeName === 'TR') {
154 } else if (child.nodeName === 'CAPTION') {
155 newElement.insertBefore(child, newElement.firstChild);
157 newElement.append(child);
162 newElement.append(tBody);
164 return newElement as HTMLElement;
169 canBeEmpty(): false {
173 isShadowRoot(): boolean {
177 setColWidths(widths: string[]) {
178 const self = this.getWritable();
179 self.__colWidths = widths;
182 getColWidths(): string[] {
183 const self = this.getLatest();
184 return [...self.__colWidths];
187 getStyles(): StyleMap {
188 const self = this.getLatest();
189 return new Map(self.__styles);
192 setStyles(styles: StyleMap): void {
193 const self = this.getWritable();
194 self.__styles = new Map(styles);
197 getCordsFromCellNode(
198 tableCellNode: TableCellNode,
199 table: TableDOMTable,
200 ): {x: number; y: number} {
201 const {rows, domRows} = table;
203 for (let y = 0; y < rows; y++) {
204 const row = domRows[y];
210 const x = row.findIndex((cell) => {
215 const cellNode = $getNearestNodeFromDOMNode(elem);
216 return cellNode === tableCellNode;
224 throw new Error('Cell not found in table.');
230 table: TableDOMTable,
231 ): null | TableDOMCell {
232 const {domRows} = table;
234 const row = domRows[y];
240 const index = x < row.length ? x : row.length - 1;
242 const cell = row[index];
251 getDOMCellFromCordsOrThrow(
254 table: TableDOMTable,
256 const cell = this.getDOMCellFromCords(x, y, table);
259 throw new Error('Cell not found at cords.');
265 getCellNodeFromCords(
268 table: TableDOMTable,
269 ): null | TableCellNode {
270 const cell = this.getDOMCellFromCords(x, y, table);
276 const node = $getNearestNodeFromDOMNode(cell.elem);
278 if ($isTableCellNode(node)) {
285 getCellNodeFromCordsOrThrow(
288 table: TableDOMTable,
290 const node = this.getCellNodeFromCords(x, y, table);
293 throw new Error('Node at cords not TableCellNode.');
299 canSelectBefore(): true {
308 export function $getElementForTableNode(
309 editor: LexicalEditor,
310 tableNode: TableNode,
312 const tableElement = editor.getElementByKey(tableNode.getKey());
314 if (tableElement == null) {
315 throw new Error('Table Element Not Found');
318 return getTable(tableElement);
321 export function $convertTableElement(element: HTMLElement): DOMConversionOutput {
322 const node = $createTableNode();
323 setCommonBlockPropsFromElement(element, node);
325 const colWidths = getTableColumnWidths(element as HTMLTableElement);
326 node.setColWidths(colWidths);
327 node.setStyles(extractStyleMapFromElement(element));
332 export function $createTableNode(): TableNode {
333 return $applyNodeReplacement(new TableNode());
336 export function $isTableNode(
337 node: LexicalNode | null | undefined,
338 ): node is TableNode {
339 return node instanceof TableNode;