diff --git a/packages/richtext-lexical/package.json b/packages/richtext-lexical/package.json index 2eb6d1338b..d3a29a4fe2 100644 --- a/packages/richtext-lexical/package.json +++ b/packages/richtext-lexical/package.json @@ -58,7 +58,7 @@ "uuid": "10.0.0" }, "devDependencies": { - "@lexical/eslint-plugin": " 0.17.0", + "@lexical/eslint-plugin": "0.17.0", "@payloadcms/eslint-config": "workspace:*", "@payloadcms/next": "workspace:*", "@payloadcms/translations": "workspace:*", diff --git a/packages/richtext-lexical/src/features/experimental_table/feature.client.ts b/packages/richtext-lexical/src/features/experimental_table/feature.client.ts index 02aa36d656..6bafdcc36c 100644 --- a/packages/richtext-lexical/src/features/experimental_table/feature.client.ts +++ b/packages/richtext-lexical/src/features/experimental_table/feature.client.ts @@ -8,6 +8,7 @@ import { slashMenuBasicGroupWithItems } from '../shared/slashMenu/basicGroup.js' import { toolbarAddDropdownGroupWithItems } from '../shared/toolbar/addDropdownGroup.js' import { TableActionMenuPlugin } from './plugins/TableActionMenuPlugin/index.js' import { TableCellResizerPlugin } from './plugins/TableCellResizerPlugin/index.js' +import { TableHoverActionsPlugin } from './plugins/TableHoverActionsPlugin/index.js' import { OPEN_TABLE_DRAWER_COMMAND, TableContext, @@ -29,6 +30,10 @@ export const TableFeatureClient = createClientFeature({ Component: TableActionMenuPlugin, position: 'floatingAnchorElem', }, + { + Component: TableHoverActionsPlugin, + position: 'floatingAnchorElem', + }, ], providers: [TableContext], slashMenu: { diff --git a/packages/richtext-lexical/src/features/experimental_table/plugins/TableCellResizerPlugin/index.scss b/packages/richtext-lexical/src/features/experimental_table/plugins/TableCellResizerPlugin/index.scss deleted file mode 100644 index a238b9a2c8..0000000000 --- a/packages/richtext-lexical/src/features/experimental_table/plugins/TableCellResizerPlugin/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -.TableCellResizer__resizer { - position: absolute; -} diff --git a/packages/richtext-lexical/src/features/experimental_table/plugins/TableCellResizerPlugin/index.tsx b/packages/richtext-lexical/src/features/experimental_table/plugins/TableCellResizerPlugin/index.tsx index 6bf90c1d32..b5476c3870 100644 --- a/packages/richtext-lexical/src/features/experimental_table/plugins/TableCellResizerPlugin/index.tsx +++ b/packages/richtext-lexical/src/features/experimental_table/plugins/TableCellResizerPlugin/index.tsx @@ -20,9 +20,9 @@ import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' -import type { PluginComponent, PluginComponentWithAnchor } from '../../../typesClient.js' +import type { PluginComponent } from '../../../typesClient.js' -import './index.scss' +import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js' type MousePosition = { x: number @@ -38,6 +38,7 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element { const targetRef = useRef(null) const resizerRef = useRef(null) const tableRectRef = useRef(null) + const editorConfig = useEditorConfigContext() const mouseStartPosRef = useRef(null) const [mouseCurrentPos, updateMouseCurrentPos] = useState(null) @@ -378,12 +379,12 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element { {activeCell != null && !isMouseDown && (
diff --git a/packages/richtext-lexical/src/features/experimental_table/plugins/TableHoverActionsPlugin/index.tsx b/packages/richtext-lexical/src/features/experimental_table/plugins/TableHoverActionsPlugin/index.tsx new file mode 100644 index 0000000000..ad5e05e993 --- /dev/null +++ b/packages/richtext-lexical/src/features/experimental_table/plugins/TableHoverActionsPlugin/index.tsx @@ -0,0 +1,244 @@ +/** + * 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, TableRowNode } from '@lexical/table' +import type { EditorConfig, NodeKey } from 'lexical' +import type { JSX } from 'react' + +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { + $getTableColumnIndexFromTableCellNode, + $getTableRowIndexFromTableCellNode, + $insertTableColumn__EXPERIMENTAL, + $insertTableRow__EXPERIMENTAL, + $isTableCellNode, + $isTableNode, + TableNode, +} from '@lexical/table' +import { $findMatchingParent, mergeRegister } from '@lexical/utils' +import { $getNearestNodeFromDOMNode } from 'lexical' +import { useEffect, useRef, useState } from 'react' +import * as React from 'react' +import { createPortal } from 'react-dom' + +import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js' +import { useDebounce } from '../../utils/useDebounce.js' + +const BUTTON_WIDTH_PX = 20 + +function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement }): JSX.Element { + const [editor] = useLexicalComposerContext() + const editorConfig = useEditorConfigContext() + const [isShownRow, setShownRow] = useState(false) + const [isShownColumn, setShownColumn] = useState(false) + const [shouldListenMouseMove, setShouldListenMouseMove] = useState(false) + const [position, setPosition] = useState({}) + const codeSetRef = useRef>(new Set()) + const tableDOMNodeRef = useRef(null) + + const debouncedOnMouseMove = useDebounce( + (event: MouseEvent) => { + const { isOutside, tableDOMNode } = getMouseInfo(event, editorConfig.editorConfig?.lexical) + + if (isOutside) { + setShownRow(false) + setShownColumn(false) + return + } + + if (!tableDOMNode) { + return + } + + tableDOMNodeRef.current = tableDOMNode + + let hoveredRowNode: TableCellNode | null = null + let hoveredColumnNode: TableCellNode | null = null + let tableDOMElement: HTMLElement | null = null + + editor.update(() => { + const maybeTableCell = $getNearestNodeFromDOMNode(tableDOMNode) + + if ($isTableCellNode(maybeTableCell)) { + const table = $findMatchingParent(maybeTableCell, (node) => $isTableNode(node)) + if (!$isTableNode(table)) { + return + } + + tableDOMElement = editor.getElementByKey(table?.getKey()) + + if (tableDOMElement) { + const rowCount = table.getChildrenSize() + const colCount = + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + ((table as TableNode).getChildAtIndex(0) as TableRowNode)?.getChildrenSize() + + const rowIndex = $getTableRowIndexFromTableCellNode(maybeTableCell) + const colIndex = $getTableColumnIndexFromTableCellNode(maybeTableCell) + + if (rowIndex === rowCount - 1) { + hoveredRowNode = maybeTableCell + } else if (colIndex === colCount - 1) { + hoveredColumnNode = maybeTableCell + } + } + } + }) + + if (tableDOMElement) { + const { + bottom: tableElemBottom, + height: tableElemHeight, + right: tableElemRight, + width: tableElemWidth, + x: tableElemX, + y: tableElemY, + } = (tableDOMElement as HTMLTableElement).getBoundingClientRect() + + const { left: editorElemLeft, y: editorElemY } = anchorElem.getBoundingClientRect() + + if (hoveredRowNode) { + setShownColumn(false) + + setShownRow(true) + setPosition({ + height: BUTTON_WIDTH_PX, + left: tableElemX - editorElemLeft, + top: tableElemBottom - editorElemY + 5, + width: tableElemWidth, + }) + } else if (hoveredColumnNode) { + setShownColumn(true) + setShownRow(false) + setPosition({ + height: tableElemHeight, + left: tableElemRight - editorElemLeft + 5, + top: tableElemY - editorElemY, + width: BUTTON_WIDTH_PX, + }) + } + } + }, + 50, + 250, + ) + + useEffect(() => { + if (!shouldListenMouseMove) { + return + } + + document.addEventListener('mousemove', debouncedOnMouseMove) + + return () => { + setShownRow(false) + setShownColumn(false) + + document.removeEventListener('mousemove', debouncedOnMouseMove) + } + }, [shouldListenMouseMove, debouncedOnMouseMove]) + + useEffect(() => { + return mergeRegister( + editor.registerMutationListener( + TableNode, + (mutations) => { + editor.getEditorState().read(() => { + for (const [key, type] of mutations) { + switch (type) { + case 'created': + codeSetRef.current.add(key) + setShouldListenMouseMove(codeSetRef.current.size > 0) + break + + case 'destroyed': + codeSetRef.current.delete(key) + setShouldListenMouseMove(codeSetRef.current.size > 0) + break + + default: + break + } + } + }) + }, + { skipInitialization: false }, + ), + ) + }, [editor]) + + const insertAction = (insertRow: boolean) => { + editor.update(() => { + if (tableDOMNodeRef.current) { + const maybeTableNode = $getNearestNodeFromDOMNode(tableDOMNodeRef.current) + maybeTableNode?.selectEnd() + if (insertRow) { + $insertTableRow__EXPERIMENTAL() + setShownRow(false) + } else { + $insertTableColumn__EXPERIMENTAL() + setShownColumn(false) + } + } + }) + } + + return ( + <> + {isShownRow && ( +