UNPKG

@lexical/react

Version:

This package provides Lexical components and hooks for React applications.

189 lines (182 loc) 7.26 kB
/** * 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 { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { TableNode, TableCellNode, TableRowNode, INSERT_TABLE_COMMAND, $createTableNodeWithDimensions, $computeTableMapSkipCellCheck, $createTableCellNode, $isTableNode, $getNodeTriplet, $computeTableMap, $isTableRowNode, $isTableCellNode, applyTableHandlers } from '@lexical/table'; import { mergeRegister, $insertNodeToNearestRoot, $insertFirst } from '@lexical/utils'; import { $isTextNode, COMMAND_PRIORITY_EDITOR, $createParagraphNode, $getNodeByKey } from 'lexical'; import { useEffect } from 'react'; /** * 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. * */ function TablePlugin({ hasCellMerge = true, hasCellBackgroundColor = true, hasTabHandler = true }) { const [editor] = useLexicalComposerContext(); useEffect(() => { if (!editor.hasNodes([TableNode, TableCellNode, TableRowNode])) { { throw Error(`TablePlugin: TableNode, TableCellNode or TableRowNode not registered on editor`); } } return mergeRegister(editor.registerCommand(INSERT_TABLE_COMMAND, ({ columns, rows, includeHeaders }) => { const tableNode = $createTableNodeWithDimensions(Number(rows), Number(columns), includeHeaders); $insertNodeToNearestRoot(tableNode); const firstDescendant = tableNode.getFirstDescendant(); if ($isTextNode(firstDescendant)) { firstDescendant.select(); } return true; }, COMMAND_PRIORITY_EDITOR), editor.registerNodeTransform(TableNode, node => { const [gridMap] = $computeTableMapSkipCellCheck(node, null, null); const maxRowLength = gridMap.reduce((curLength, row) => { return Math.max(curLength, row.length); }, 0); const rowNodes = node.getChildren(); for (let i = 0; i < gridMap.length; ++i) { const rowNode = rowNodes[i]; if (!rowNode) { continue; } const rowLength = gridMap[i].reduce((acc, cell) => cell ? 1 + acc : acc, 0); if (rowLength === maxRowLength) { continue; } for (let j = rowLength; j < maxRowLength; ++j) { // TODO: inherit header state from another header or body const newCell = $createTableCellNode(0); newCell.append($createParagraphNode()); rowNode.append(newCell); } } })); }, [editor]); useEffect(() => { const tableSelections = new Map(); const initializeTableNode = (tableNode, nodeKey, dom) => { const tableElement = dom; const tableSelection = applyTableHandlers(tableNode, tableElement, editor, hasTabHandler); tableSelections.set(nodeKey, [tableSelection, tableElement]); }; const unregisterMutationListener = editor.registerMutationListener(TableNode, nodeMutations => { for (const [nodeKey, mutation] of nodeMutations) { if (mutation === 'created' || mutation === 'updated') { const tableSelection = tableSelections.get(nodeKey); const dom = editor.getElementByKey(nodeKey); if (!(tableSelection && dom === tableSelection[1])) { // The update created a new DOM node, destroy the existing TableObserver if (tableSelection) { tableSelection[0].removeListeners(); tableSelections.delete(nodeKey); } if (dom !== null) { // Create a new TableObserver editor.getEditorState().read(() => { const tableNode = $getNodeByKey(nodeKey); if ($isTableNode(tableNode)) { initializeTableNode(tableNode, nodeKey, dom); } }); } } } else if (mutation === 'destroyed') { const tableSelection = tableSelections.get(nodeKey); if (tableSelection !== undefined) { tableSelection[0].removeListeners(); tableSelections.delete(nodeKey); } } } }, { skipInitialization: false }); return () => { unregisterMutationListener(); // Hook might be called multiple times so cleaning up tables listeners as well, // as it'll be reinitialized during recurring call for (const [, [tableSelection]] of tableSelections) { tableSelection.removeListeners(); } }; }, [editor, hasTabHandler]); // Unmerge cells when the feature isn't enabled useEffect(() => { if (hasCellMerge) { return; } return editor.registerNodeTransform(TableCellNode, node => { if (node.getColSpan() > 1 || node.getRowSpan() > 1) { // When we have rowSpan we have to map the entire Table to understand where the new Cells // fit best; let's analyze all Cells at once to save us from further transform iterations const [,, gridNode] = $getNodeTriplet(node); const [gridMap] = $computeTableMap(gridNode, node, node); // TODO this function expects Tables to be normalized. Look into this once it exists const rowsCount = gridMap.length; const columnsCount = gridMap[0].length; let row = gridNode.getFirstChild(); if (!$isTableRowNode(row)) { throw Error(`Expected TableNode first child to be a RowNode`); } const unmerged = []; for (let i = 0; i < rowsCount; i++) { if (i !== 0) { row = row.getNextSibling(); if (!$isTableRowNode(row)) { throw Error(`Expected TableNode first child to be a RowNode`); } } let lastRowCell = null; for (let j = 0; j < columnsCount; j++) { const cellMap = gridMap[i][j]; const cell = cellMap.cell; if (cellMap.startRow === i && cellMap.startColumn === j) { lastRowCell = cell; unmerged.push(cell); } else if (cell.getColSpan() > 1 || cell.getRowSpan() > 1) { if (!$isTableCellNode(cell)) { throw Error(`Expected TableNode cell to be a TableCellNode`); } const newCell = $createTableCellNode(cell.__headerState); if (lastRowCell !== null) { lastRowCell.insertAfter(newCell); } else { $insertFirst(row, newCell); } } } } for (const cell of unmerged) { cell.setColSpan(1); cell.setRowSpan(1); } } }); }, [editor, hasCellMerge]); // Remove cell background color when feature is disabled useEffect(() => { if (hasCellBackgroundColor) { return; } return editor.registerNodeTransform(TableCellNode, node => { if (node.getBackgroundColor() !== null) { node.setBackgroundColor(null); } }); }, [editor, hasCellBackgroundColor, hasCellMerge]); return null; } export { TablePlugin };