| 
									
										
										
										
											2024-09-18 20:43:39 +08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * 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 {TableCellNode} from './LexicalTableCellNode'; | 
					
						
							|  |  |  | import type { | 
					
						
							|  |  |  |   DOMConversionMap, | 
					
						
							|  |  |  |   DOMConversionOutput, | 
					
						
							|  |  |  |   DOMExportOutput, | 
					
						
							|  |  |  |   EditorConfig, | 
					
						
							|  |  |  |   LexicalEditor, | 
					
						
							|  |  |  |   LexicalNode, | 
					
						
							|  |  |  |   NodeKey, | 
					
						
							|  |  |  |   SerializedElementNode, | 
					
						
							|  |  |  | } from 'lexical'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import {addClassNamesToElement, isHTMLElement} from '@lexical/utils'; | 
					
						
							|  |  |  | import { | 
					
						
							|  |  |  |   $applyNodeReplacement, | 
					
						
							|  |  |  |   $getNearestNodeFromDOMNode, | 
					
						
							|  |  |  |   ElementNode, | 
					
						
							|  |  |  | } from 'lexical'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import {$isTableCellNode} from './LexicalTableCellNode'; | 
					
						
							|  |  |  | import {TableDOMCell, TableDOMTable} from './LexicalTableObserver'; | 
					
						
							|  |  |  | import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode'; | 
					
						
							|  |  |  | import {getTable} from './LexicalTableSelectionHelpers'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export type SerializedTableNode = SerializedElementNode; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** @noInheritDoc */ | 
					
						
							|  |  |  | export class TableNode extends ElementNode { | 
					
						
							|  |  |  |   static getType(): string { | 
					
						
							|  |  |  |     return 'table'; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static clone(node: TableNode): TableNode { | 
					
						
							|  |  |  |     return new TableNode(node.__key); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static importDOM(): DOMConversionMap | null { | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       table: (_node: Node) => ({ | 
					
						
							|  |  |  |         conversion: $convertTableElement, | 
					
						
							|  |  |  |         priority: 1, | 
					
						
							|  |  |  |       }), | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static importJSON(_serializedNode: SerializedTableNode): TableNode { | 
					
						
							|  |  |  |     return $createTableNode(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   constructor(key?: NodeKey) { | 
					
						
							|  |  |  |     super(key); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   exportJSON(): SerializedElementNode { | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       ...super.exportJSON(), | 
					
						
							|  |  |  |       type: 'table', | 
					
						
							|  |  |  |       version: 1, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   createDOM(config: EditorConfig, editor?: LexicalEditor): HTMLElement { | 
					
						
							|  |  |  |     const tableElement = document.createElement('table'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     addClassNamesToElement(tableElement, config.theme.table); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return tableElement; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   updateDOM(): boolean { | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   exportDOM(editor: LexicalEditor): DOMExportOutput { | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       ...super.exportDOM(editor), | 
					
						
							|  |  |  |       after: (tableElement) => { | 
					
						
							| 
									
										
										
										
											2024-10-05 19:42:47 +08:00
										 |  |  |         if (!tableElement) { | 
					
						
							|  |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-09-18 20:43:39 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-05 19:42:47 +08:00
										 |  |  |         const newElement = tableElement.cloneNode() as ParentNode; | 
					
						
							|  |  |  |         const tBody = document.createElement('tbody'); | 
					
						
							| 
									
										
										
										
											2024-09-18 20:43:39 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-05 19:42:47 +08:00
										 |  |  |         if (isHTMLElement(tableElement)) { | 
					
						
							|  |  |  |           for (const child of Array.from(tableElement.children)) { | 
					
						
							|  |  |  |             if (child.nodeName === 'TR') { | 
					
						
							|  |  |  |               tBody.append(child); | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |               newElement.append(child); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2024-09-18 20:43:39 +08:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2024-10-05 19:42:47 +08:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-09-18 20:43:39 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-05 19:42:47 +08:00
										 |  |  |         newElement.append(tBody); | 
					
						
							| 
									
										
										
										
											2024-09-18 20:43:39 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-05 19:42:47 +08:00
										 |  |  |         return newElement as HTMLElement; | 
					
						
							| 
									
										
										
										
											2024-09-18 20:43:39 +08:00
										 |  |  |       }, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   canBeEmpty(): false { | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   isShadowRoot(): boolean { | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getCordsFromCellNode( | 
					
						
							|  |  |  |     tableCellNode: TableCellNode, | 
					
						
							|  |  |  |     table: TableDOMTable, | 
					
						
							|  |  |  |   ): {x: number; y: number} { | 
					
						
							|  |  |  |     const {rows, domRows} = table; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for (let y = 0; y < rows; y++) { | 
					
						
							|  |  |  |       const row = domRows[y]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (row == null) { | 
					
						
							|  |  |  |         continue; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const x = row.findIndex((cell) => { | 
					
						
							|  |  |  |         if (!cell) { | 
					
						
							|  |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         const {elem} = cell; | 
					
						
							|  |  |  |         const cellNode = $getNearestNodeFromDOMNode(elem); | 
					
						
							|  |  |  |         return cellNode === tableCellNode; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (x !== -1) { | 
					
						
							|  |  |  |         return {x, y}; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     throw new Error('Cell not found in table.'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getDOMCellFromCords( | 
					
						
							|  |  |  |     x: number, | 
					
						
							|  |  |  |     y: number, | 
					
						
							|  |  |  |     table: TableDOMTable, | 
					
						
							|  |  |  |   ): null | TableDOMCell { | 
					
						
							|  |  |  |     const {domRows} = table; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const row = domRows[y]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (row == null) { | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const index = x < row.length ? x : row.length - 1; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const cell = row[index]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (cell == null) { | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return cell; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getDOMCellFromCordsOrThrow( | 
					
						
							|  |  |  |     x: number, | 
					
						
							|  |  |  |     y: number, | 
					
						
							|  |  |  |     table: TableDOMTable, | 
					
						
							|  |  |  |   ): TableDOMCell { | 
					
						
							|  |  |  |     const cell = this.getDOMCellFromCords(x, y, table); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!cell) { | 
					
						
							|  |  |  |       throw new Error('Cell not found at cords.'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return cell; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getCellNodeFromCords( | 
					
						
							|  |  |  |     x: number, | 
					
						
							|  |  |  |     y: number, | 
					
						
							|  |  |  |     table: TableDOMTable, | 
					
						
							|  |  |  |   ): null | TableCellNode { | 
					
						
							|  |  |  |     const cell = this.getDOMCellFromCords(x, y, table); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (cell == null) { | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const node = $getNearestNodeFromDOMNode(cell.elem); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if ($isTableCellNode(node)) { | 
					
						
							|  |  |  |       return node; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getCellNodeFromCordsOrThrow( | 
					
						
							|  |  |  |     x: number, | 
					
						
							|  |  |  |     y: number, | 
					
						
							|  |  |  |     table: TableDOMTable, | 
					
						
							|  |  |  |   ): TableCellNode { | 
					
						
							|  |  |  |     const node = this.getCellNodeFromCords(x, y, table); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!node) { | 
					
						
							|  |  |  |       throw new Error('Node at cords not TableCellNode.'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return node; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   canSelectBefore(): true { | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   canIndent(): false { | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function $getElementForTableNode( | 
					
						
							|  |  |  |   editor: LexicalEditor, | 
					
						
							|  |  |  |   tableNode: TableNode, | 
					
						
							|  |  |  | ): TableDOMTable { | 
					
						
							|  |  |  |   const tableElement = editor.getElementByKey(tableNode.getKey()); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (tableElement == null) { | 
					
						
							|  |  |  |     throw new Error('Table Element Not Found'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return getTable(tableElement); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function $convertTableElement(_domNode: Node): DOMConversionOutput { | 
					
						
							|  |  |  |   return {node: $createTableNode()}; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function $createTableNode(): TableNode { | 
					
						
							|  |  |  |   return $applyNodeReplacement(new TableNode()); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function $isTableNode( | 
					
						
							|  |  |  |   node: LexicalNode | null | undefined, | 
					
						
							|  |  |  | ): node is TableNode { | 
					
						
							|  |  |  |   return node instanceof TableNode; | 
					
						
							|  |  |  | } |