/**
* 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 {JSX} from 'react';
import {Signal, signal} from '@lexical/extension';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {
$isScrollableTablesActive,
registerTableCellUnmergeTransform,
registerTablePlugin,
registerTableSelectionObserver,
setScrollableTablesActive,
TableCellNode,
} from '@lexical/table';
import {$fullReconcile} from 'lexical';
import {useEffect, useState} from 'react';
export interface TablePluginProps {
/**
* When `false` (default `true`), merged cell support (colspan and rowspan) will be disabled and all
* tables will be forced into a regular grid with 1x1 table cells.
*/
hasCellMerge?: boolean;
/**
* When `false` (default `true`), the background color of TableCellNode will always be removed.
*/
hasCellBackgroundColor?: boolean;
/**
* When `true` (default `true`), the tab key can be used to navigate table cells.
*/
hasTabHandler?: boolean;
/**
* When `true` (default `false`), tables will be wrapped in a `
` to enable horizontal scrolling
*/
hasHorizontalScroll?: boolean;
/**
* When `true` (default `false`), nested tables will be allowed.
*
* @experimental Nested tables are not officially supported.
*/
hasNestedTables?: boolean;
}
/**
* A plugin to enable all of the features of Lexical's TableNode.
*
* @param props - See type for documentation
* @returns An element to render in your LexicalComposer
*/
export function TablePlugin({
hasCellMerge = true,
hasCellBackgroundColor = true,
hasTabHandler = true,
hasHorizontalScroll = false,
hasNestedTables = false,
}: TablePluginProps): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
const hadHorizontalScroll = $isScrollableTablesActive(editor);
if (hadHorizontalScroll !== hasHorizontalScroll) {
setScrollableTablesActive(editor, hasHorizontalScroll);
// Re-render existing tables through the new scroll-wrapper config without
// cloning every TableNode the way marking them dirty would. A full
// reconcile marks no nodes dirty, so it's deferred (no synchronous render
// from this effect) and produces no history entry.
editor.update($fullReconcile);
}
}, [editor, hasHorizontalScroll]);
const hasNestedTablesSignal = usePropSignal(hasNestedTables);
useEffect(
() => registerTablePlugin(editor, {hasNestedTables: hasNestedTablesSignal}),
[editor, hasNestedTablesSignal],
);
useEffect(
() => registerTableSelectionObserver(editor, hasTabHandler),
[editor, hasTabHandler],
);
// Unmerge cells when the feature isn't enabled
useEffect(() => {
if (!hasCellMerge) {
return registerTableCellUnmergeTransform(editor);
}
}, [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;
}
function usePropSignal(value: T): Signal {
const [configSignal] = useState(() => signal(value));
if (configSignal.peek() !== value) {
// eslint-disable-next-line react-hooks/immutability
configSignal.value = value;
}
return configSignal;
}