702 lines
20 KiB
TypeScript
702 lines
20 KiB
TypeScript
/**
|
|
* This source code is derived from code from Meta Platforms, Inc.
|
|
* and affiliates, licensed under the MIT license located in the
|
|
* LICENSE file in the /app/soapbox/features/compose/editor directory.
|
|
*/
|
|
|
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
import useLexicalEditable from '@lexical/react/useLexicalEditable';
|
|
import {
|
|
$deleteTableColumn__EXPERIMENTAL,
|
|
$deleteTableRow__EXPERIMENTAL,
|
|
$getTableCellNodeFromLexicalNode,
|
|
$getTableColumnIndexFromTableCellNode,
|
|
$getTableNodeFromLexicalNodeOrThrow,
|
|
$getTableRowIndexFromTableCellNode,
|
|
$insertTableColumn__EXPERIMENTAL,
|
|
$insertTableRow__EXPERIMENTAL,
|
|
$isTableCellNode,
|
|
$isTableRowNode,
|
|
$unmergeCell,
|
|
getTableSelectionFromTableElement,
|
|
HTMLTableElementWithWithTableSelectionState,
|
|
TableCellHeaderStates,
|
|
TableCellNode,
|
|
type TableRowNode,
|
|
} from '@lexical/table';
|
|
import {
|
|
$createParagraphNode,
|
|
$getRoot,
|
|
$getSelection,
|
|
$isElementNode,
|
|
$isParagraphNode,
|
|
$isRangeSelection,
|
|
$isTextNode,
|
|
DEPRECATED_$getNodeTriplet,
|
|
DEPRECATED_$isGridCellNode,
|
|
DEPRECATED_$isGridSelection,
|
|
GridSelection,
|
|
} from 'lexical';
|
|
import * as React from 'react';
|
|
import { ReactPortal, useCallback, useEffect, useRef, useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
|
|
import type { DEPRECATED_GridCellNode, ElementNode } from 'lexical';
|
|
|
|
function computeSelectionCount(selection: GridSelection): {
|
|
columns: number
|
|
rows: number
|
|
} {
|
|
const selectionShape = selection.getShape();
|
|
return {
|
|
columns: selectionShape.toX - selectionShape.fromX + 1,
|
|
rows: selectionShape.toY - selectionShape.fromY + 1,
|
|
};
|
|
}
|
|
|
|
// This is important when merging cells as there is no good way to re-merge weird shapes (a result
|
|
// of selecting merged cells and non-merged)
|
|
function isGridSelectionRectangular(selection: GridSelection): boolean {
|
|
const nodes = selection.getNodes();
|
|
const currentRows: Array<number> = [];
|
|
let currentRow: TableRowNode | null = null;
|
|
let expectedColumns: number | null = null;
|
|
let currentColumns = 0;
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const node = nodes[i];
|
|
if ($isTableCellNode(node)) {
|
|
const row = node.getParentOrThrow();
|
|
if (!$isTableRowNode(row)) throw new Error('Expected CellNode to have a RowNode parent');
|
|
if (currentRow !== row) {
|
|
if (expectedColumns !== null && currentColumns !== expectedColumns) {
|
|
return false;
|
|
}
|
|
if (currentRow !== null) {
|
|
expectedColumns = currentColumns;
|
|
}
|
|
currentRow = row;
|
|
currentColumns = 0;
|
|
}
|
|
const colSpan = node.__colSpan;
|
|
for (let j = 0; j < colSpan; j++) {
|
|
if (currentRows[currentColumns + j] === undefined) {
|
|
currentRows[currentColumns + j] = 0;
|
|
}
|
|
currentRows[currentColumns + j] += node.__rowSpan;
|
|
}
|
|
currentColumns += colSpan;
|
|
}
|
|
}
|
|
return (
|
|
(expectedColumns === null || currentColumns === expectedColumns) &&
|
|
currentRows.every((v) => v === currentRows[0])
|
|
);
|
|
}
|
|
|
|
function $canUnmerge(): boolean {
|
|
const selection = $getSelection();
|
|
if (
|
|
($isRangeSelection(selection) && !selection.isCollapsed()) ||
|
|
(DEPRECATED_$isGridSelection(selection) &&
|
|
!selection.anchor.is(selection.focus)) ||
|
|
(!$isRangeSelection(selection) && !DEPRECATED_$isGridSelection(selection))
|
|
) {
|
|
return false;
|
|
}
|
|
const [cell] = DEPRECATED_$getNodeTriplet(selection.anchor);
|
|
return cell.__colSpan > 1 || cell.__rowSpan > 1;
|
|
}
|
|
|
|
function $cellContainsEmptyParagraph(cell: DEPRECATED_GridCellNode): boolean {
|
|
if (cell.getChildrenSize() !== 1) {
|
|
return false;
|
|
}
|
|
const firstChild = cell.getFirstChildOrThrow();
|
|
if (!$isParagraphNode(firstChild) || !firstChild.isEmpty()) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function $selectLastDescendant(node: ElementNode): void {
|
|
const lastDescendant = node.getLastDescendant();
|
|
if ($isTextNode(lastDescendant)) {
|
|
lastDescendant.select();
|
|
} else if ($isElementNode(lastDescendant)) {
|
|
lastDescendant.selectEnd();
|
|
} else if (lastDescendant !== null) {
|
|
lastDescendant.selectNext();
|
|
}
|
|
}
|
|
|
|
type TableCellActionMenuProps = Readonly<{
|
|
contextRef: {current: null | HTMLElement}
|
|
onClose: () => void
|
|
setIsMenuOpen: (isOpen: boolean) => void
|
|
tableCellNode: TableCellNode
|
|
cellMerge: boolean
|
|
}>;
|
|
|
|
function TableActionMenu({
|
|
onClose,
|
|
tableCellNode: _tableCellNode,
|
|
setIsMenuOpen,
|
|
contextRef,
|
|
cellMerge,
|
|
}: TableCellActionMenuProps) {
|
|
const [editor] = useLexicalComposerContext();
|
|
const dropDownRef = useRef<HTMLDivElement | null>(null);
|
|
const [tableCellNode, updateTableCellNode] = useState(_tableCellNode);
|
|
const [selectionCounts, updateSelectionCounts] = useState({
|
|
columns: 1,
|
|
rows: 1,
|
|
});
|
|
const [canMergeCells, setCanMergeCells] = useState(false);
|
|
const [canUnmergeCell, setCanUnmergeCell] = useState(false);
|
|
|
|
useEffect(() => {
|
|
return editor.registerMutationListener(TableCellNode, (nodeMutations) => {
|
|
const nodeUpdated =
|
|
nodeMutations.get(tableCellNode.getKey()) === 'updated';
|
|
|
|
if (nodeUpdated) {
|
|
editor.getEditorState().read(() => {
|
|
updateTableCellNode(tableCellNode.getLatest());
|
|
});
|
|
}
|
|
});
|
|
}, [editor, tableCellNode]);
|
|
|
|
useEffect(() => {
|
|
editor.getEditorState().read(() => {
|
|
const selection = $getSelection();
|
|
// Merge cells
|
|
if (DEPRECATED_$isGridSelection(selection)) {
|
|
const currentSelectionCounts = computeSelectionCount(selection);
|
|
updateSelectionCounts(computeSelectionCount(selection));
|
|
setCanMergeCells(
|
|
isGridSelectionRectangular(selection) &&
|
|
(currentSelectionCounts.columns > 1 ||
|
|
currentSelectionCounts.rows > 1),
|
|
);
|
|
}
|
|
// Unmerge cell
|
|
setCanUnmergeCell($canUnmerge());
|
|
});
|
|
}, [editor]);
|
|
|
|
useEffect(() => {
|
|
const menuButtonElement = contextRef.current;
|
|
const dropDownElement = dropDownRef.current;
|
|
const rootElement = editor.getRootElement();
|
|
|
|
if (menuButtonElement && dropDownElement && rootElement) {
|
|
const rootEleRect = rootElement.getBoundingClientRect();
|
|
const menuButtonRect = menuButtonElement.getBoundingClientRect();
|
|
dropDownElement.style.opacity = '1';
|
|
const dropDownElementRect = dropDownElement.getBoundingClientRect();
|
|
const margin = 5;
|
|
let leftPosition = menuButtonRect.right + margin;
|
|
if (
|
|
leftPosition + dropDownElementRect.width > window.innerWidth ||
|
|
leftPosition + dropDownElementRect.width > rootEleRect.right
|
|
) {
|
|
const position =
|
|
menuButtonRect.left - dropDownElementRect.width - margin;
|
|
leftPosition = (position < 0 ? margin : position) + window.pageXOffset;
|
|
}
|
|
dropDownElement.style.left = `${leftPosition + window.pageXOffset}px`;
|
|
|
|
let topPosition = menuButtonRect.top;
|
|
if (topPosition + dropDownElementRect.height > window.innerHeight) {
|
|
const position = menuButtonRect.bottom - dropDownElementRect.height;
|
|
topPosition = (position < 0 ? margin : position) + window.pageYOffset;
|
|
}
|
|
dropDownElement.style.top = `${topPosition + +window.pageYOffset}px`;
|
|
}
|
|
}, [contextRef, dropDownRef, editor]);
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (
|
|
dropDownRef.current &&
|
|
contextRef.current &&
|
|
!dropDownRef.current.contains(event.target as Node) &&
|
|
!contextRef.current.contains(event.target as Node)
|
|
) {
|
|
setIsMenuOpen(false);
|
|
}
|
|
}
|
|
|
|
window.addEventListener('click', handleClickOutside);
|
|
|
|
return () => window.removeEventListener('click', handleClickOutside);
|
|
}, [setIsMenuOpen, contextRef]);
|
|
|
|
const clearTableSelection = useCallback(() => {
|
|
editor.update(() => {
|
|
if (tableCellNode.isAttached()) {
|
|
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
|
|
const tableElement = editor.getElementByKey(
|
|
tableNode.getKey(),
|
|
) as HTMLTableElementWithWithTableSelectionState;
|
|
|
|
if (!tableElement) {
|
|
throw new Error('Expected to find tableElement in DOM');
|
|
}
|
|
|
|
const tableSelection = getTableSelectionFromTableElement(tableElement);
|
|
if (tableSelection !== null) {
|
|
tableSelection.clearHighlight();
|
|
}
|
|
|
|
tableNode.markDirty();
|
|
updateTableCellNode(tableCellNode.getLatest());
|
|
}
|
|
|
|
const rootNode = $getRoot();
|
|
rootNode.selectStart();
|
|
});
|
|
}, [editor, tableCellNode]);
|
|
|
|
const mergeTableCellsAtSelection = () => {
|
|
editor.update(() => {
|
|
const selection = $getSelection();
|
|
if (DEPRECATED_$isGridSelection(selection)) {
|
|
const { columns, rows } = computeSelectionCount(selection);
|
|
const nodes = selection.getNodes();
|
|
let firstCell: null | DEPRECATED_GridCellNode = null;
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const node = nodes[i];
|
|
if (DEPRECATED_$isGridCellNode(node)) {
|
|
if (firstCell === null) {
|
|
node.setColSpan(columns).setRowSpan(rows);
|
|
firstCell = node;
|
|
const isEmpty = $cellContainsEmptyParagraph(node);
|
|
let firstChild;
|
|
if (
|
|
isEmpty &&
|
|
$isParagraphNode((firstChild = node.getFirstChild()))
|
|
) {
|
|
firstChild.remove();
|
|
}
|
|
} else if (DEPRECATED_$isGridCellNode(firstCell)) {
|
|
const isEmpty = $cellContainsEmptyParagraph(node);
|
|
if (!isEmpty) {
|
|
firstCell.append(...node.getChildren());
|
|
}
|
|
node.remove();
|
|
}
|
|
}
|
|
}
|
|
if (firstCell !== null) {
|
|
if (firstCell.getChildrenSize() === 0) {
|
|
firstCell.append($createParagraphNode());
|
|
}
|
|
$selectLastDescendant(firstCell);
|
|
}
|
|
onClose();
|
|
}
|
|
});
|
|
};
|
|
|
|
const unmergeTableCellsAtSelection = () => {
|
|
editor.update(() => {
|
|
$unmergeCell();
|
|
});
|
|
};
|
|
|
|
const insertTableRowAtSelection = useCallback(
|
|
(shouldInsertAfter: boolean) => {
|
|
editor.update(() => {
|
|
$insertTableRow__EXPERIMENTAL(shouldInsertAfter);
|
|
onClose();
|
|
});
|
|
},
|
|
[editor, onClose],
|
|
);
|
|
|
|
const insertTableColumnAtSelection = useCallback(
|
|
(shouldInsertAfter: boolean) => {
|
|
editor.update(() => {
|
|
for (let i = 0; i < selectionCounts.columns; i++) {
|
|
$insertTableColumn__EXPERIMENTAL(shouldInsertAfter);
|
|
}
|
|
onClose();
|
|
});
|
|
},
|
|
[editor, onClose, selectionCounts.columns],
|
|
);
|
|
|
|
const deleteTableRowAtSelection = useCallback(() => {
|
|
editor.update(() => {
|
|
$deleteTableRow__EXPERIMENTAL();
|
|
onClose();
|
|
});
|
|
}, [editor, onClose]);
|
|
|
|
const deleteTableAtSelection = useCallback(() => {
|
|
editor.update(() => {
|
|
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
|
|
tableNode.remove();
|
|
|
|
clearTableSelection();
|
|
onClose();
|
|
});
|
|
}, [editor, tableCellNode, clearTableSelection, onClose]);
|
|
|
|
const deleteTableColumnAtSelection = useCallback(() => {
|
|
editor.update(() => {
|
|
$deleteTableColumn__EXPERIMENTAL();
|
|
onClose();
|
|
});
|
|
}, [editor, onClose]);
|
|
|
|
const toggleTableRowIsHeader = useCallback(() => {
|
|
editor.update(() => {
|
|
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
|
|
|
|
const tableRowIndex = $getTableRowIndexFromTableCellNode(tableCellNode);
|
|
|
|
const tableRows = tableNode.getChildren();
|
|
|
|
if (tableRowIndex >= tableRows.length || tableRowIndex < 0) {
|
|
throw new Error('Expected table cell to be inside of table row.');
|
|
}
|
|
|
|
const tableRow = tableRows[tableRowIndex];
|
|
|
|
if (!$isTableRowNode(tableRow)) {
|
|
throw new Error('Expected table row');
|
|
}
|
|
|
|
tableRow.getChildren().forEach((tableCell) => {
|
|
if (!$isTableCellNode(tableCell)) {
|
|
throw new Error('Expected table cell');
|
|
}
|
|
|
|
tableCell.toggleHeaderStyle(TableCellHeaderStates.ROW);
|
|
});
|
|
|
|
clearTableSelection();
|
|
onClose();
|
|
});
|
|
}, [editor, tableCellNode, clearTableSelection, onClose]);
|
|
|
|
const toggleTableColumnIsHeader = useCallback(() => {
|
|
editor.update(() => {
|
|
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
|
|
|
|
const tableColumnIndex =
|
|
$getTableColumnIndexFromTableCellNode(tableCellNode);
|
|
|
|
const tableRows = tableNode.getChildren();
|
|
|
|
for (let r = 0; r < tableRows.length; r++) {
|
|
const tableRow = tableRows[r];
|
|
|
|
if (!$isTableRowNode(tableRow)) {
|
|
throw new Error('Expected table row');
|
|
}
|
|
|
|
const tableCells = tableRow.getChildren();
|
|
|
|
if (tableColumnIndex >= tableCells.length || tableColumnIndex < 0) {
|
|
throw new Error('Expected table cell to be inside of table row.');
|
|
}
|
|
|
|
const tableCell = tableCells[tableColumnIndex];
|
|
|
|
if (!$isTableCellNode(tableCell)) {
|
|
throw new Error('Expected table cell');
|
|
}
|
|
|
|
tableCell.toggleHeaderStyle(TableCellHeaderStates.COLUMN);
|
|
}
|
|
|
|
clearTableSelection();
|
|
onClose();
|
|
});
|
|
}, [editor, tableCellNode, clearTableSelection, onClose]);
|
|
|
|
let mergeCellButton: null | JSX.Element = null;
|
|
if (cellMerge) {
|
|
if (canMergeCells) {
|
|
mergeCellButton = (
|
|
<button
|
|
className='item'
|
|
onClick={() => mergeTableCellsAtSelection()}
|
|
data-test-id='table-merge-cells'
|
|
>
|
|
Merge cells
|
|
</button>
|
|
);
|
|
} else if (canUnmergeCell) {
|
|
mergeCellButton = (
|
|
<button
|
|
className='item'
|
|
onClick={() => unmergeTableCellsAtSelection()}
|
|
data-test-id='table-unmerge-cells'
|
|
>
|
|
Unmerge cells
|
|
</button>
|
|
);
|
|
}
|
|
}
|
|
|
|
return createPortal(
|
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
<div
|
|
className='dropdown'
|
|
ref={dropDownRef}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
}}
|
|
>
|
|
{mergeCellButton}
|
|
<hr />
|
|
<button
|
|
className='item'
|
|
onClick={() => insertTableRowAtSelection(false)}
|
|
data-test-id='table-insert-row-above'
|
|
>
|
|
<span className='text'>
|
|
Insert{' '}
|
|
{selectionCounts.rows === 1 ? 'row' : `${selectionCounts.rows} rows`}{' '}
|
|
above
|
|
</span>
|
|
</button>
|
|
<button
|
|
className='item'
|
|
onClick={() => insertTableRowAtSelection(true)}
|
|
data-test-id='table-insert-row-below'
|
|
>
|
|
<span className='text'>
|
|
Insert{' '}
|
|
{selectionCounts.rows === 1 ? 'row' : `${selectionCounts.rows} rows`}{' '}
|
|
below
|
|
</span>
|
|
</button>
|
|
<hr />
|
|
<button
|
|
className='item'
|
|
onClick={() => insertTableColumnAtSelection(false)}
|
|
data-test-id='table-insert-column-before'
|
|
>
|
|
<span className='text'>
|
|
Insert{' '}
|
|
{selectionCounts.columns === 1
|
|
? 'column'
|
|
: `${selectionCounts.columns} columns`}{' '}
|
|
left
|
|
</span>
|
|
</button>
|
|
<button
|
|
className='item'
|
|
onClick={() => insertTableColumnAtSelection(true)}
|
|
data-test-id='table-insert-column-after'
|
|
>
|
|
<span className='text'>
|
|
Insert{' '}
|
|
{selectionCounts.columns === 1
|
|
? 'column'
|
|
: `${selectionCounts.columns} columns`}{' '}
|
|
right
|
|
</span>
|
|
</button>
|
|
<hr />
|
|
<button
|
|
className='item'
|
|
onClick={() => deleteTableColumnAtSelection()}
|
|
data-test-id='table-delete-columns'
|
|
>
|
|
<span className='text'>Delete column</span>
|
|
</button>
|
|
<button
|
|
className='item'
|
|
onClick={() => deleteTableRowAtSelection()}
|
|
data-test-id='table-delete-rows'
|
|
>
|
|
<span className='text'>Delete row</span>
|
|
</button>
|
|
<button
|
|
className='item'
|
|
onClick={() => deleteTableAtSelection()}
|
|
data-test-id='table-delete'
|
|
>
|
|
<span className='text'>Delete table</span>
|
|
</button>
|
|
<hr />
|
|
<button className='item' onClick={() => toggleTableRowIsHeader()}>
|
|
<span className='text'>
|
|
{(tableCellNode.__headerState & TableCellHeaderStates.ROW) ===
|
|
TableCellHeaderStates.ROW
|
|
? 'Remove'
|
|
: 'Add'}{' '}
|
|
row header
|
|
</span>
|
|
</button>
|
|
<button className='item' onClick={() => toggleTableColumnIsHeader()}>
|
|
<span className='text'>
|
|
{(tableCellNode.__headerState & TableCellHeaderStates.COLUMN) ===
|
|
TableCellHeaderStates.COLUMN
|
|
? 'Remove'
|
|
: 'Add'}{' '}
|
|
column header
|
|
</span>
|
|
</button>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|
|
|
|
function TableCellActionMenuContainer({
|
|
anchorElem,
|
|
cellMerge,
|
|
}: {
|
|
anchorElem: HTMLElement
|
|
cellMerge: boolean
|
|
}): JSX.Element {
|
|
const [editor] = useLexicalComposerContext();
|
|
|
|
const menuButtonRef = useRef(null);
|
|
const menuRootRef = useRef(null);
|
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
|
|
const [tableCellNode, setTableMenuCellNode] = useState<TableCellNode | null>(
|
|
null,
|
|
);
|
|
|
|
const moveMenu = useCallback(() => {
|
|
const menu = menuButtonRef.current;
|
|
const selection = $getSelection();
|
|
const nativeSelection = window.getSelection();
|
|
const activeElement = document.activeElement;
|
|
|
|
if (!selection || !menu) {
|
|
setTableMenuCellNode(null);
|
|
return;
|
|
}
|
|
|
|
const rootElement = editor.getRootElement();
|
|
|
|
if (
|
|
$isRangeSelection(selection) &&
|
|
rootElement !== null &&
|
|
nativeSelection !== null &&
|
|
rootElement.contains(nativeSelection.anchorNode)
|
|
) {
|
|
const tableCellNodeFromSelection = $getTableCellNodeFromLexicalNode(
|
|
selection.anchor.getNode(),
|
|
);
|
|
|
|
if (!tableCellNodeFromSelection) {
|
|
setTableMenuCellNode(null);
|
|
return;
|
|
}
|
|
|
|
const tableCellParentNodeDOM = editor.getElementByKey(
|
|
tableCellNodeFromSelection.getKey(),
|
|
);
|
|
|
|
if (!tableCellParentNodeDOM) {
|
|
setTableMenuCellNode(null);
|
|
return;
|
|
}
|
|
|
|
setTableMenuCellNode(tableCellNodeFromSelection);
|
|
} else if (!activeElement) {
|
|
setTableMenuCellNode(null);
|
|
}
|
|
}, [editor]);
|
|
|
|
useEffect(() => {
|
|
return editor.registerUpdateListener(() => {
|
|
editor.getEditorState().read(() => {
|
|
moveMenu();
|
|
});
|
|
});
|
|
});
|
|
|
|
useEffect(() => {
|
|
const menuButtonDOM = menuButtonRef.current as HTMLButtonElement | null;
|
|
|
|
if (menuButtonDOM && tableCellNode) {
|
|
const tableCellNodeDOM = editor.getElementByKey(tableCellNode.getKey());
|
|
|
|
if (tableCellNodeDOM) {
|
|
const tableCellRect = tableCellNodeDOM.getBoundingClientRect();
|
|
const menuRect = menuButtonDOM.getBoundingClientRect();
|
|
const anchorRect = anchorElem.getBoundingClientRect();
|
|
|
|
const top = tableCellRect.top - anchorRect.top + 4;
|
|
const left =
|
|
tableCellRect.right - menuRect.width - 10 - anchorRect.left;
|
|
|
|
menuButtonDOM.style.opacity = '1';
|
|
menuButtonDOM.style.transform = `translate(${left}px, ${top}px)`;
|
|
} else {
|
|
menuButtonDOM.style.opacity = '0';
|
|
menuButtonDOM.style.transform = 'translate(-10000px, -10000px)';
|
|
}
|
|
}
|
|
}, [menuButtonRef, tableCellNode, editor, anchorElem]);
|
|
|
|
const prevTableCellDOM = useRef(tableCellNode);
|
|
|
|
useEffect(() => {
|
|
if (prevTableCellDOM.current !== tableCellNode) {
|
|
setIsMenuOpen(false);
|
|
}
|
|
|
|
prevTableCellDOM.current = tableCellNode;
|
|
}, [prevTableCellDOM, tableCellNode]);
|
|
|
|
return (
|
|
<div className='table-cell-action-button-container' ref={menuButtonRef}>
|
|
{tableCellNode && (
|
|
<>
|
|
<button
|
|
className='table-cell-action-button chevron-down'
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setIsMenuOpen(!isMenuOpen);
|
|
}}
|
|
ref={menuRootRef}
|
|
>
|
|
<i className='chevron-down' />
|
|
</button>
|
|
{isMenuOpen && (
|
|
<TableActionMenu
|
|
contextRef={menuRootRef}
|
|
setIsMenuOpen={setIsMenuOpen}
|
|
onClose={() => setIsMenuOpen(false)}
|
|
tableCellNode={tableCellNode}
|
|
cellMerge={cellMerge}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function TableActionMenuPlugin({
|
|
anchorElem,
|
|
cellMerge = false,
|
|
}: {
|
|
anchorElem?: HTMLElement | null
|
|
cellMerge?: boolean
|
|
}): null | ReactPortal {
|
|
const isEditable = useLexicalEditable();
|
|
return createPortal(
|
|
isEditable ? (
|
|
<TableCellActionMenuContainer
|
|
anchorElem={anchorElem || document.body}
|
|
cellMerge={cellMerge}
|
|
/>
|
|
) : null,
|
|
anchorElem || document.body,
|
|
);
|
|
}
|