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.
17 SerializedElementNode,
21 import {addClassNamesToElement} from '@lexical/utils';
23 $applyNodeReplacement,
31 import {COLUMN_WIDTH, PIXEL_VALUE_REG_EXP} from './constants';
33 export const TableCellHeaderStates = {
40 export type TableCellHeaderState =
41 typeof TableCellHeaderStates[keyof typeof TableCellHeaderStates];
43 export type SerializedTableCellNode = Spread<
47 headerState: TableCellHeaderState;
49 backgroundColor?: null | string;
55 export class TableCellNode extends ElementNode {
61 __headerState: TableCellHeaderState;
65 __backgroundColor: null | string;
67 static getType(): string {
71 static clone(node: TableCellNode): TableCellNode {
72 const cellNode = new TableCellNode(
78 cellNode.__rowSpan = node.__rowSpan;
79 cellNode.__backgroundColor = node.__backgroundColor;
83 static importDOM(): DOMConversionMap | null {
85 td: (node: Node) => ({
86 conversion: $convertTableCellNodeElement,
89 th: (node: Node) => ({
90 conversion: $convertTableCellNodeElement,
96 static importJSON(serializedNode: SerializedTableCellNode): TableCellNode {
97 const colSpan = serializedNode.colSpan || 1;
98 const rowSpan = serializedNode.rowSpan || 1;
99 const cellNode = $createTableCellNode(
100 serializedNode.headerState,
102 serializedNode.width || undefined,
104 cellNode.__rowSpan = rowSpan;
105 cellNode.__backgroundColor = serializedNode.backgroundColor || null;
110 headerState = TableCellHeaderStates.NO_STATUS,
116 this.__colSpan = colSpan;
118 this.__headerState = headerState;
119 this.__width = width;
120 this.__backgroundColor = null;
123 createDOM(config: EditorConfig): HTMLElement {
124 const element = document.createElement(
126 ) as HTMLTableCellElement;
129 element.style.width = `${this.__width}px`;
131 if (this.__colSpan > 1) {
132 element.colSpan = this.__colSpan;
134 if (this.__rowSpan > 1) {
135 element.rowSpan = this.__rowSpan;
137 if (this.__backgroundColor !== null) {
138 element.style.backgroundColor = this.__backgroundColor;
141 addClassNamesToElement(
143 config.theme.tableCell,
144 this.hasHeader() && config.theme.tableCellHeader,
150 exportDOM(editor: LexicalEditor): DOMExportOutput {
151 const {element} = super.exportDOM(editor);
154 const element_ = element as HTMLTableCellElement;
155 element_.style.border = '1px solid black';
156 if (this.__colSpan > 1) {
157 element_.colSpan = this.__colSpan;
159 if (this.__rowSpan > 1) {
160 element_.rowSpan = this.__rowSpan;
162 element_.style.width = `${this.getWidth() || COLUMN_WIDTH}px`;
164 element_.style.verticalAlign = 'top';
165 element_.style.textAlign = 'start';
167 const backgroundColor = this.getBackgroundColor();
168 if (backgroundColor !== null) {
169 element_.style.backgroundColor = backgroundColor;
170 } else if (this.hasHeader()) {
171 element_.style.backgroundColor = '#f2f3f5';
180 exportJSON(): SerializedTableCellNode {
182 ...super.exportJSON(),
183 backgroundColor: this.getBackgroundColor(),
184 colSpan: this.__colSpan,
185 headerState: this.__headerState,
186 rowSpan: this.__rowSpan,
188 width: this.getWidth(),
192 getColSpan(): number {
193 return this.__colSpan;
196 setColSpan(colSpan: number): this {
197 this.getWritable().__colSpan = colSpan;
201 getRowSpan(): number {
202 return this.__rowSpan;
205 setRowSpan(rowSpan: number): this {
206 this.getWritable().__rowSpan = rowSpan;
211 return this.hasHeader() ? 'th' : 'td';
214 setHeaderStyles(headerState: TableCellHeaderState): TableCellHeaderState {
215 const self = this.getWritable();
216 self.__headerState = headerState;
217 return this.__headerState;
220 getHeaderStyles(): TableCellHeaderState {
221 return this.getLatest().__headerState;
224 setWidth(width: number): number | null | undefined {
225 const self = this.getWritable();
226 self.__width = width;
230 getWidth(): number | undefined {
231 return this.getLatest().__width;
234 getBackgroundColor(): null | string {
235 return this.getLatest().__backgroundColor;
238 setBackgroundColor(newBackgroundColor: null | string): void {
239 this.getWritable().__backgroundColor = newBackgroundColor;
242 toggleHeaderStyle(headerStateToToggle: TableCellHeaderState): TableCellNode {
243 const self = this.getWritable();
245 if ((self.__headerState & headerStateToToggle) === headerStateToToggle) {
246 self.__headerState -= headerStateToToggle;
248 self.__headerState += headerStateToToggle;
254 hasHeaderState(headerState: TableCellHeaderState): boolean {
255 return (this.getHeaderStyles() & headerState) === headerState;
258 hasHeader(): boolean {
259 return this.getLatest().__headerState !== TableCellHeaderStates.NO_STATUS;
262 updateDOM(prevNode: TableCellNode): boolean {
264 prevNode.__headerState !== this.__headerState ||
265 prevNode.__width !== this.__width ||
266 prevNode.__colSpan !== this.__colSpan ||
267 prevNode.__rowSpan !== this.__rowSpan ||
268 prevNode.__backgroundColor !== this.__backgroundColor
272 isShadowRoot(): boolean {
276 collapseAtStart(): true {
280 canBeEmpty(): false {
289 export function $convertTableCellNodeElement(
291 ): DOMConversionOutput {
292 const domNode_ = domNode as HTMLTableCellElement;
293 const nodeName = domNode.nodeName.toLowerCase();
295 let width: number | undefined = undefined;
297 if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) {
298 width = parseFloat(domNode_.style.width);
301 const tableCellNode = $createTableCellNode(
303 ? TableCellHeaderStates.ROW
304 : TableCellHeaderStates.NO_STATUS,
309 tableCellNode.__rowSpan = domNode_.rowSpan;
310 const backgroundColor = domNode_.style.backgroundColor;
311 if (backgroundColor !== '') {
312 tableCellNode.__backgroundColor = backgroundColor;
315 const style = domNode_.style;
316 const textDecoration = style.textDecoration.split(' ');
317 const hasBoldFontWeight =
318 style.fontWeight === '700' || style.fontWeight === 'bold';
319 const hasLinethroughTextDecoration = textDecoration.includes('line-through');
320 const hasItalicFontStyle = style.fontStyle === 'italic';
321 const hasUnderlineTextDecoration = textDecoration.includes('underline');
323 after: (childLexicalNodes) => {
324 if (childLexicalNodes.length === 0) {
325 childLexicalNodes.push($createParagraphNode());
327 return childLexicalNodes;
329 forChild: (lexicalNode, parentLexicalNode) => {
330 if ($isTableCellNode(parentLexicalNode) && !$isElementNode(lexicalNode)) {
331 const paragraphNode = $createParagraphNode();
333 $isLineBreakNode(lexicalNode) &&
334 lexicalNode.getTextContent() === '\n'
338 if ($isTextNode(lexicalNode)) {
339 if (hasBoldFontWeight) {
340 lexicalNode.toggleFormat('bold');
342 if (hasLinethroughTextDecoration) {
343 lexicalNode.toggleFormat('strikethrough');
345 if (hasItalicFontStyle) {
346 lexicalNode.toggleFormat('italic');
348 if (hasUnderlineTextDecoration) {
349 lexicalNode.toggleFormat('underline');
352 paragraphNode.append(lexicalNode);
353 return paragraphNode;
362 export function $createTableCellNode(
363 headerState: TableCellHeaderState,
367 return $applyNodeReplacement(new TableCellNode(headerState, colSpan, width));
370 export function $isTableCellNode(
371 node: LexicalNode | null | undefined,
372 ): node is TableCellNode {
373 return node instanceof TableCellNode;