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 {extractStyleMapFromElement, StyleMap} from "../../utils/dom";
32 import {CommonBlockAlignment, extractAlignmentFromElement} from "../../nodes/_common";
34 export const TableCellHeaderStates = {
41 export type TableCellHeaderState =
42 typeof TableCellHeaderStates[keyof typeof TableCellHeaderStates];
44 export type SerializedTableCellNode = Spread<
48 headerState: TableCellHeaderState;
50 backgroundColor?: null | string;
51 styles: Record<string, string>;
52 alignment: CommonBlockAlignment;
58 export class TableCellNode extends ElementNode {
64 __headerState: TableCellHeaderState;
68 __backgroundColor: null | string;
70 __styles: StyleMap = new Map;
72 __alignment: CommonBlockAlignment = '';
74 static getType(): string {
78 static clone(node: TableCellNode): TableCellNode {
79 const cellNode = new TableCellNode(
85 cellNode.__rowSpan = node.__rowSpan;
86 cellNode.__backgroundColor = node.__backgroundColor;
87 cellNode.__styles = new Map(node.__styles);
88 cellNode.__alignment = node.__alignment;
92 static importDOM(): DOMConversionMap | null {
94 td: (node: Node) => ({
95 conversion: $convertTableCellNodeElement,
98 th: (node: Node) => ({
99 conversion: $convertTableCellNodeElement,
105 static importJSON(serializedNode: SerializedTableCellNode): TableCellNode {
106 const node = $createTableCellNode(
107 serializedNode.headerState,
108 serializedNode.colSpan,
109 serializedNode.width,
112 if (serializedNode.rowSpan) {
113 node.setRowSpan(serializedNode.rowSpan);
116 node.setStyles(new Map(Object.entries(serializedNode.styles)));
117 node.setAlignment(serializedNode.alignment);
123 headerState = TableCellHeaderStates.NO_STATUS,
129 this.__colSpan = colSpan;
131 this.__headerState = headerState;
132 this.__width = width;
133 this.__backgroundColor = null;
136 createDOM(config: EditorConfig): HTMLElement {
137 const element = document.createElement(
139 ) as HTMLTableCellElement;
142 element.style.width = `${this.__width}px`;
144 if (this.__colSpan > 1) {
145 element.colSpan = this.__colSpan;
147 if (this.__rowSpan > 1) {
148 element.rowSpan = this.__rowSpan;
150 if (this.__backgroundColor !== null) {
151 element.style.backgroundColor = this.__backgroundColor;
154 addClassNamesToElement(
156 config.theme.tableCell,
157 this.hasHeader() && config.theme.tableCellHeader,
160 for (const [name, value] of this.__styles.entries()) {
161 element.style.setProperty(name, value);
164 if (this.__alignment) {
165 element.classList.add('align-' + this.__alignment);
171 exportDOM(editor: LexicalEditor): DOMExportOutput {
172 const {element} = super.exportDOM(editor);
178 exportJSON(): SerializedTableCellNode {
180 ...super.exportJSON(),
181 backgroundColor: this.getBackgroundColor(),
182 colSpan: this.__colSpan,
183 headerState: this.__headerState,
184 rowSpan: this.__rowSpan,
186 width: this.getWidth(),
187 styles: Object.fromEntries(this.__styles),
188 alignment: this.__alignment,
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;
235 const self = this.getWritable();
236 self.__width = undefined;
239 getStyles(): StyleMap {
240 const self = this.getLatest();
241 return new Map(self.__styles);
244 setStyles(styles: StyleMap): void {
245 const self = this.getWritable();
246 self.__styles = new Map(styles);
249 setAlignment(alignment: CommonBlockAlignment) {
250 const self = this.getWritable();
251 self.__alignment = alignment;
254 getAlignment(): CommonBlockAlignment {
255 const self = this.getLatest();
256 return self.__alignment;
259 updateTag(tag: string): void {
260 const isHeader = tag.toLowerCase() === 'th';
261 const state = isHeader ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS;
262 const self = this.getWritable();
263 self.__headerState = state;
266 getBackgroundColor(): null | string {
267 return this.getLatest().__backgroundColor;
270 setBackgroundColor(newBackgroundColor: null | string): void {
271 this.getWritable().__backgroundColor = newBackgroundColor;
274 toggleHeaderStyle(headerStateToToggle: TableCellHeaderState): TableCellNode {
275 const self = this.getWritable();
277 if ((self.__headerState & headerStateToToggle) === headerStateToToggle) {
278 self.__headerState -= headerStateToToggle;
280 self.__headerState += headerStateToToggle;
286 hasHeaderState(headerState: TableCellHeaderState): boolean {
287 return (this.getHeaderStyles() & headerState) === headerState;
290 hasHeader(): boolean {
291 return this.getLatest().__headerState !== TableCellHeaderStates.NO_STATUS;
294 updateDOM(prevNode: TableCellNode): boolean {
296 prevNode.__headerState !== this.__headerState ||
297 prevNode.__width !== this.__width ||
298 prevNode.__colSpan !== this.__colSpan ||
299 prevNode.__rowSpan !== this.__rowSpan ||
300 prevNode.__backgroundColor !== this.__backgroundColor ||
301 prevNode.__styles !== this.__styles ||
302 prevNode.__alignment !== this.__alignment
306 isShadowRoot(): boolean {
310 collapseAtStart(): true {
314 canBeEmpty(): false {
323 export function $convertTableCellNodeElement(
325 ): DOMConversionOutput {
326 const domNode_ = domNode as HTMLTableCellElement;
327 const nodeName = domNode.nodeName.toLowerCase();
329 let width: number | undefined = undefined;
332 const PIXEL_VALUE_REG_EXP = /^(\d+(?:\.\d+)?)px$/;
333 if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) {
334 width = parseFloat(domNode_.style.width);
337 const tableCellNode = $createTableCellNode(
339 ? TableCellHeaderStates.ROW
340 : TableCellHeaderStates.NO_STATUS,
345 tableCellNode.__rowSpan = domNode_.rowSpan;
347 const style = domNode_.style;
348 const textDecoration = style.textDecoration.split(' ');
349 const hasBoldFontWeight =
350 style.fontWeight === '700' || style.fontWeight === 'bold';
351 const hasLinethroughTextDecoration = textDecoration.includes('line-through');
352 const hasItalicFontStyle = style.fontStyle === 'italic';
353 const hasUnderlineTextDecoration = textDecoration.includes('underline');
355 if (domNode instanceof HTMLElement) {
356 tableCellNode.setStyles(extractStyleMapFromElement(domNode));
357 tableCellNode.setAlignment(extractAlignmentFromElement(domNode));
361 after: (childLexicalNodes) => {
362 if (childLexicalNodes.length === 0) {
363 childLexicalNodes.push($createParagraphNode());
365 return childLexicalNodes;
367 forChild: (lexicalNode, parentLexicalNode) => {
368 if ($isTableCellNode(parentLexicalNode) && !$isElementNode(lexicalNode)) {
369 const paragraphNode = $createParagraphNode();
371 $isLineBreakNode(lexicalNode) &&
372 lexicalNode.getTextContent() === '\n'
376 if ($isTextNode(lexicalNode)) {
377 if (hasBoldFontWeight) {
378 lexicalNode.toggleFormat('bold');
380 if (hasLinethroughTextDecoration) {
381 lexicalNode.toggleFormat('strikethrough');
383 if (hasItalicFontStyle) {
384 lexicalNode.toggleFormat('italic');
386 if (hasUnderlineTextDecoration) {
387 lexicalNode.toggleFormat('underline');
390 paragraphNode.append(lexicalNode);
391 return paragraphNode;
400 export function $createTableCellNode(
401 headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
405 return $applyNodeReplacement(new TableCellNode(headerState, colSpan, width));
408 export function $isTableCellNode(
409 node: LexicalNode | null | undefined,
410 ): node is TableCellNode {
411 return node instanceof TableCellNode;