diff --git a/packages/richtext-lexical/package.json b/packages/richtext-lexical/package.json
index 227b829d0..d3a29a4fe 100644
--- a/packages/richtext-lexical/package.json
+++ b/packages/richtext-lexical/package.json
@@ -41,24 +41,24 @@
"translateNewKeys": "tsx scripts/translateNewKeys.ts"
},
"dependencies": {
- "@lexical/headless": "0.16.1",
- "@lexical/link": "0.16.1",
- "@lexical/list": "0.16.1",
- "@lexical/mark": "0.16.1",
- "@lexical/markdown": "0.16.1",
- "@lexical/react": "0.16.1",
- "@lexical/rich-text": "0.16.1",
- "@lexical/selection": "0.16.1",
- "@lexical/utils": "0.16.1",
+ "@lexical/headless": "0.17.0",
+ "@lexical/link": "0.17.0",
+ "@lexical/list": "0.17.0",
+ "@lexical/mark": "0.17.0",
+ "@lexical/markdown": "0.17.0",
+ "@lexical/react": "0.17.0",
+ "@lexical/rich-text": "0.17.0",
+ "@lexical/selection": "0.17.0",
+ "@lexical/utils": "0.17.0",
"@types/uuid": "10.0.0",
"bson-objectid": "2.0.4",
"dequal": "2.0.3",
- "lexical": "0.16.1",
+ "lexical": "0.17.0",
"react-error-boundary": "4.0.13",
"uuid": "10.0.0"
},
"devDependencies": {
- "@lexical/eslint-plugin": " 0.16.1",
+ "@lexical/eslint-plugin": "0.17.0",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/next": "workspace:*",
"@payloadcms/translations": "workspace:*",
@@ -75,20 +75,20 @@
"peerDependencies": {
"@faceless-ui/modal": "3.0.0-beta.2",
"@faceless-ui/scroll-info": "2.0.0-beta.0",
- "@lexical/headless": "0.16.1",
- "@lexical/link": "0.16.1",
- "@lexical/list": "0.16.1",
- "@lexical/mark": "0.16.1",
- "@lexical/markdown": "0.16.1",
- "@lexical/react": "0.16.1",
- "@lexical/rich-text": "0.16.1",
- "@lexical/selection": "0.16.1",
- "@lexical/table": "0.16.1",
- "@lexical/utils": "0.16.1",
+ "@lexical/headless": "0.17.0",
+ "@lexical/link": "0.17.0",
+ "@lexical/list": "0.17.0",
+ "@lexical/mark": "0.17.0",
+ "@lexical/markdown": "0.17.0",
+ "@lexical/react": "0.17.0",
+ "@lexical/rich-text": "0.17.0",
+ "@lexical/selection": "0.17.0",
+ "@lexical/table": "0.17.0",
+ "@lexical/utils": "0.17.0",
"@payloadcms/next": "workspace:*",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
- "lexical": "0.16.1",
+ "lexical": "0.17.0",
"payload": "workspace:*",
"react": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610"
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 02aa36d65..6bafdcc36 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/feature.server.ts b/packages/richtext-lexical/src/features/experimental_table/feature.server.ts
index 85915c85b..11c98824a 100644
--- a/packages/richtext-lexical/src/features/experimental_table/feature.server.ts
+++ b/packages/richtext-lexical/src/features/experimental_table/feature.server.ts
@@ -6,6 +6,8 @@ import { sanitizeFields } from 'payload'
// eslint-disable-next-line payload/no-imports-from-exports-dir
import { TableFeatureClient } from '../../exports/client/index.js'
import { createServerFeature } from '../../utilities/createServerFeature.js'
+import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
+import { createNode } from '../typeUtilities.js'
const fields: Field[] = [
{
@@ -41,15 +43,75 @@ export const EXPERIMENTAL_TableFeature = createServerFeature({
return schemaMap
},
nodes: [
- {
+ createNode({
+ converters: {
+ html: {
+ converter: async ({ converters, node, parent, req }) => {
+ const childrenText = await convertLexicalNodesToHTML({
+ converters,
+ lexicalNodes: node.children,
+ parent: {
+ ...node,
+ parent,
+ },
+ req,
+ })
+ return `
`
+ },
+ nodeTypes: [TableNode.getType()],
+ },
+ },
node: TableNode,
- },
- {
+ }),
+ createNode({
+ converters: {
+ html: {
+ converter: async ({ converters, node, parent, req }) => {
+ const childrenText = await convertLexicalNodesToHTML({
+ converters,
+ lexicalNodes: node.children,
+ parent: {
+ ...node,
+ parent,
+ },
+ req,
+ })
+
+ const tagName = node.headerState > 0 ? 'th' : 'td'
+ const headerStateClass = `lexical-table-cell-header-${node.headerState}`
+ const backgroundColor = node.backgroundColor
+ ? `background-color: ${node.backgroundColor};`
+ : ''
+ const colSpan = node.colSpan > 1 ? `colspan="${node.colSpan}"` : ''
+ const rowSpan = node.rowSpan > 1 ? `rowspan="${node.rowSpan}"` : ''
+
+ return `<${tagName} class="lexical-table-cell ${headerStateClass}" style="border: 1px solid #ccc; padding: 8px; ${backgroundColor}" ${colSpan} ${rowSpan}>${childrenText}${tagName}>`
+ },
+ nodeTypes: [TableCellNode.getType()],
+ },
+ },
node: TableCellNode,
- },
- {
+ }),
+ createNode({
+ converters: {
+ html: {
+ converter: async ({ converters, node, parent, req }) => {
+ const childrenText = await convertLexicalNodesToHTML({
+ converters,
+ lexicalNodes: node.children,
+ parent: {
+ ...node,
+ parent,
+ },
+ req,
+ })
+ return `${childrenText}
`
+ },
+ nodeTypes: [TableRowNode.getType()],
+ },
+ },
node: TableRowNode,
- },
+ }),
],
}
},
diff --git a/packages/richtext-lexical/src/features/experimental_table/plugins/TableActionMenuPlugin/index.tsx b/packages/richtext-lexical/src/features/experimental_table/plugins/TableActionMenuPlugin/index.tsx
index a01aa0ea2..c715a3100 100644
--- a/packages/richtext-lexical/src/features/experimental_table/plugins/TableActionMenuPlugin/index.tsx
+++ b/packages/richtext-lexical/src/features/experimental_table/plugins/TableActionMenuPlugin/index.tsx
@@ -160,15 +160,19 @@ function TableActionMenu({
const { y } = useScrollInfo()
useEffect(() => {
- return editor.registerMutationListener(TableCellNode, (nodeMutations) => {
- const nodeUpdated = nodeMutations.get(tableCellNode.getKey()) === 'updated'
+ return editor.registerMutationListener(
+ TableCellNode,
+ (nodeMutations) => {
+ const nodeUpdated = nodeMutations.get(tableCellNode.getKey()) === 'updated'
- if (nodeUpdated) {
- editor.getEditorState().read(() => {
- updateTableCellNode(tableCellNode.getLatest())
- })
- }
- })
+ if (nodeUpdated) {
+ editor.getEditorState().read(() => {
+ updateTableCellNode(tableCellNode.getLatest())
+ })
+ }
+ },
+ { skipInitialization: true },
+ )
}, [editor, tableCellNode])
useEffect(() => {
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 a238b9a2c..000000000
--- 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 6bf90c1d3..899520ee4 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)
@@ -117,14 +118,18 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element {
}, 0)
}
- document.addEventListener('mousemove', onMouseMove)
- document.addEventListener('mousedown', onMouseDown)
- document.addEventListener('mouseup', onMouseUp)
+ const removeRootListener = editor.registerRootListener((rootElement, prevRootElement) => {
+ rootElement?.addEventListener('mousemove', onMouseMove)
+ rootElement?.addEventListener('mousedown', onMouseDown)
+ rootElement?.addEventListener('mouseup', onMouseUp)
+
+ prevRootElement?.removeEventListener('mousemove', onMouseMove)
+ prevRootElement?.removeEventListener('mousedown', onMouseDown)
+ prevRootElement?.removeEventListener('mouseup', onMouseUp)
+ })
return () => {
- document.removeEventListener('mousemove', onMouseMove)
- document.removeEventListener('mousedown', onMouseDown)
- document.removeEventListener('mouseup', onMouseUp)
+ removeRootListener()
}
}, [activeCell, draggingDirection, editor, resetState])
@@ -378,12 +383,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 000000000..23de437ad
--- /dev/null
+++ b/packages/richtext-lexical/src/features/experimental_table/plugins/TableHoverActionsPlugin/index.tsx
@@ -0,0 +1,243 @@
+/**
+ * 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 && (
+