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') {
142 } else if (child.nodeName === 'CAPTION') {
143 newElement.insertBefore(child, newElement.firstChild);
145 newElement.append(child);
150 newElement.append(tBody);
152 return newElement as HTMLElement;
157 canBeEmpty(): false {
161 isShadowRoot(): boolean {
165 setColWidths(widths: string[]) {
166 const self = this.getWritable();
167 self.__colWidths = widths;
170 getColWidths(): string[] {
171 const self = this.getLatest();
172 return [...self.__colWidths];
175 getStyles(): StyleMap {
176 const self = this.getLatest();
177 return new Map(self.__styles);
180 setStyles(styles: StyleMap): void {
181 const self = this.getWritable();
182 self.__styles = new Map(styles);
185 getCordsFromCellNode(
186 tableCellNode: TableCellNode,
187 table: TableDOMTable,
188 ): {x: number; y: number} {
189 const {rows, domRows} = table;
191 for (let y = 0; y < rows; y++) {
192 const row = domRows[y];
198 const x = row.findIndex((cell) => {
203 const cellNode = $getNearestNodeFromDOMNode(elem);
204 return cellNode === tableCellNode;
212 throw new Error('Cell not found in table.');
218 table: TableDOMTable,
219 ): null | TableDOMCell {
220 const {domRows} = table;
222 const row = domRows[y];
228 const index = x < row.length ? x : row.length - 1;
230 const cell = row[index];
239 getDOMCellFromCordsOrThrow(
242 table: TableDOMTable,
244 const cell = this.getDOMCellFromCords(x, y, table);
247 throw new Error('Cell not found at cords.');
253 getCellNodeFromCords(
256 table: TableDOMTable,
257 ): null | TableCellNode {
258 const cell = this.getDOMCellFromCords(x, y, table);
264 const node = $getNearestNodeFromDOMNode(cell.elem);
266 if ($isTableCellNode(node)) {
273 getCellNodeFromCordsOrThrow(
276 table: TableDOMTable,
278 const node = this.getCellNodeFromCords(x, y, table);
281 throw new Error('Node at cords not TableCellNode.');
287 canSelectBefore(): true {
296 export function $getElementForTableNode(
297 editor: LexicalEditor,
298 tableNode: TableNode,
300 const tableElement = editor.getElementByKey(tableNode.getKey());
302 if (tableElement == null) {
303 throw new Error('Table Element Not Found');
306 return getTable(tableElement);
309 export function $convertTableElement(element: HTMLElement): DOMConversionOutput {
310 const node = $createTableNode();
311 setCommonBlockPropsFromElement(element, node);
313 const colWidths = getTableColumnWidths(element as HTMLTableElement);
314 node.setColWidths(colWidths);
315 node.setStyles(extractStyleMapFromElement(element));
320 export function $createTableNode(): TableNode {
321 return $applyNodeReplacement(new TableNode());
324 export function $isTableNode(
325 node: LexicalNode | null | undefined,
326 ): node is TableNode {
327 return node instanceof TableNode;