/** * 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 { DOMConversionMap, DOMConversionOutput, DOMExportOutput, EditorConfig, LexicalEditor, LexicalNode, NodeKey, SerializedElementNode, Spread, } from 'lexical'; import {addClassNamesToElement} from '@lexical/utils'; import { $applyNodeReplacement, $createParagraphNode, $isElementNode, $isLineBreakNode, $isTextNode, ElementNode, } from 'lexical'; import {COLUMN_WIDTH, PIXEL_VALUE_REG_EXP} from './constants'; export const TableCellHeaderStates = { BOTH: 3, COLUMN: 2, NO_STATUS: 0, ROW: 1, }; export type TableCellHeaderState = typeof TableCellHeaderStates[keyof typeof TableCellHeaderStates]; export type SerializedTableCellNode = Spread< { colSpan?: number; rowSpan?: number; headerState: TableCellHeaderState; width?: number; backgroundColor?: null | string; }, SerializedElementNode >; /** @noInheritDoc */ export class TableCellNode extends ElementNode { /** @internal */ __colSpan: number; /** @internal */ __rowSpan: number; /** @internal */ __headerState: TableCellHeaderState; /** @internal */ __width?: number; /** @internal */ __backgroundColor: null | string; static getType(): string { return 'tablecell'; } static clone(node: TableCellNode): TableCellNode { const cellNode = new TableCellNode( node.__headerState, node.__colSpan, node.__width, node.__key, ); cellNode.__rowSpan = node.__rowSpan; cellNode.__backgroundColor = node.__backgroundColor; return cellNode; } static importDOM(): DOMConversionMap | null { return { td: (node: Node) => ({ conversion: $convertTableCellNodeElement, priority: 0, }), th: (node: Node) => ({ conversion: $convertTableCellNodeElement, priority: 0, }), }; } static importJSON(serializedNode: SerializedTableCellNode): TableCellNode { const colSpan = serializedNode.colSpan || 1; const rowSpan = serializedNode.rowSpan || 1; const cellNode = $createTableCellNode( serializedNode.headerState, colSpan, serializedNode.width || undefined, ); cellNode.__rowSpan = rowSpan; cellNode.__backgroundColor = serializedNode.backgroundColor || null; return cellNode; } constructor( headerState = TableCellHeaderStates.NO_STATUS, colSpan = 1, width?: number, key?: NodeKey, ) { super(key); this.__colSpan = colSpan; this.__rowSpan = 1; this.__headerState = headerState; this.__width = width; this.__backgroundColor = null; } createDOM(config: EditorConfig): HTMLElement { const element = document.createElement( this.getTag(), ) as HTMLTableCellElement; if (this.__width) { element.style.width = `${this.__width}px`; } if (this.__colSpan > 1) { element.colSpan = this.__colSpan; } if (this.__rowSpan > 1) { element.rowSpan = this.__rowSpan; } if (this.__backgroundColor !== null) { element.style.backgroundColor = this.__backgroundColor; } addClassNamesToElement( element, config.theme.tableCell, this.hasHeader() && config.theme.tableCellHeader, ); return element; } exportDOM(editor: LexicalEditor): DOMExportOutput { const {element} = super.exportDOM(editor); if (element) { const element_ = element as HTMLTableCellElement; element_.style.border = '1px solid black'; if (this.__colSpan > 1) { element_.colSpan = this.__colSpan; } if (this.__rowSpan > 1) { element_.rowSpan = this.__rowSpan; } element_.style.width = `${this.getWidth() || COLUMN_WIDTH}px`; element_.style.verticalAlign = 'top'; element_.style.textAlign = 'start'; const backgroundColor = this.getBackgroundColor(); if (backgroundColor !== null) { element_.style.backgroundColor = backgroundColor; } else if (this.hasHeader()) { element_.style.backgroundColor = '#f2f3f5'; } } return { element, }; } exportJSON(): SerializedTableCellNode { return { ...super.exportJSON(), backgroundColor: this.getBackgroundColor(), colSpan: this.__colSpan, headerState: this.__headerState, rowSpan: this.__rowSpan, type: 'tablecell', width: this.getWidth(), }; } getColSpan(): number { return this.__colSpan; } setColSpan(colSpan: number): this { this.getWritable().__colSpan = colSpan; return this; } getRowSpan(): number { return this.__rowSpan; } setRowSpan(rowSpan: number): this { this.getWritable().__rowSpan = rowSpan; return this; } getTag(): string { return this.hasHeader() ? 'th' : 'td'; } setHeaderStyles(headerState: TableCellHeaderState): TableCellHeaderState { const self = this.getWritable(); self.__headerState = headerState; return this.__headerState; } getHeaderStyles(): TableCellHeaderState { return this.getLatest().__headerState; } setWidth(width: number): number | null | undefined { const self = this.getWritable(); self.__width = width; return this.__width; } getWidth(): number | undefined { return this.getLatest().__width; } getBackgroundColor(): null | string { return this.getLatest().__backgroundColor; } setBackgroundColor(newBackgroundColor: null | string): void { this.getWritable().__backgroundColor = newBackgroundColor; } toggleHeaderStyle(headerStateToToggle: TableCellHeaderState): TableCellNode { const self = this.getWritable(); if ((self.__headerState & headerStateToToggle) === headerStateToToggle) { self.__headerState -= headerStateToToggle; } else { self.__headerState += headerStateToToggle; } return self; } hasHeaderState(headerState: TableCellHeaderState): boolean { return (this.getHeaderStyles() & headerState) === headerState; } hasHeader(): boolean { return this.getLatest().__headerState !== TableCellHeaderStates.NO_STATUS; } updateDOM(prevNode: TableCellNode): boolean { return ( prevNode.__headerState !== this.__headerState || prevNode.__width !== this.__width || prevNode.__colSpan !== this.__colSpan || prevNode.__rowSpan !== this.__rowSpan || prevNode.__backgroundColor !== this.__backgroundColor ); } isShadowRoot(): boolean { return true; } collapseAtStart(): true { return true; } canBeEmpty(): false { return false; } canIndent(): false { return false; } } export function $convertTableCellNodeElement( domNode: Node, ): DOMConversionOutput { const domNode_ = domNode as HTMLTableCellElement; const nodeName = domNode.nodeName.toLowerCase(); let width: number | undefined = undefined; if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) { width = parseFloat(domNode_.style.width); } const tableCellNode = $createTableCellNode( nodeName === 'th' ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS, domNode_.colSpan, width, ); tableCellNode.__rowSpan = domNode_.rowSpan; const backgroundColor = domNode_.style.backgroundColor; if (backgroundColor !== '') { tableCellNode.__backgroundColor = backgroundColor; } const style = domNode_.style; const textDecoration = style.textDecoration.split(' '); const hasBoldFontWeight = style.fontWeight === '700' || style.fontWeight === 'bold'; const hasLinethroughTextDecoration = textDecoration.includes('line-through'); const hasItalicFontStyle = style.fontStyle === 'italic'; const hasUnderlineTextDecoration = textDecoration.includes('underline'); return { after: (childLexicalNodes) => { if (childLexicalNodes.length === 0) { childLexicalNodes.push($createParagraphNode()); } return childLexicalNodes; }, forChild: (lexicalNode, parentLexicalNode) => { if ($isTableCellNode(parentLexicalNode) && !$isElementNode(lexicalNode)) { const paragraphNode = $createParagraphNode(); if ( $isLineBreakNode(lexicalNode) && lexicalNode.getTextContent() === '\n' ) { return null; } if ($isTextNode(lexicalNode)) { if (hasBoldFontWeight) { lexicalNode.toggleFormat('bold'); } if (hasLinethroughTextDecoration) { lexicalNode.toggleFormat('strikethrough'); } if (hasItalicFontStyle) { lexicalNode.toggleFormat('italic'); } if (hasUnderlineTextDecoration) { lexicalNode.toggleFormat('underline'); } } paragraphNode.append(lexicalNode); return paragraphNode; } return lexicalNode; }, node: tableCellNode, }; } export function $createTableCellNode( headerState: TableCellHeaderState, colSpan = 1, width?: number, ): TableCellNode { return $applyNodeReplacement(new TableCellNode(headerState, colSpan, width)); } export function $isTableCellNode( node: LexicalNode | null | undefined, ): node is TableCellNode { return node instanceof TableCellNode; }