From a30eeaf644aed35df2cfb5995452e226c9af965e Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Mon, 11 Nov 2024 21:58:28 -0700 Subject: [PATCH] feat(richtext-lexical): backport relevant from lexical playground between 0.18.0 and 0.20.0 (#9129) --- .../plugins/TableHoverActionsPlugin/index.tsx | 63 ++++++++++++++----- .../features/format/bold/feature.client.tsx | 3 +- .../format/inlineCode/feature.client.tsx | 3 +- .../features/format/italic/feature.client.tsx | 3 +- .../format/strikethrough/feature.client.tsx | 3 +- .../format/subscript/feature.client.tsx | 3 +- .../format/superscript/feature.client.tsx | 3 +- .../format/underline/feature.client.tsx | 3 +- .../LexicalMenu.tsx | 13 +++- .../LexicalTypeaheadMenuPlugin/index.tsx | 2 +- 10 files changed, 74 insertions(+), 25 deletions(-) diff --git a/packages/richtext-lexical/src/features/experimental_table/client/plugins/TableHoverActionsPlugin/index.tsx b/packages/richtext-lexical/src/features/experimental_table/client/plugins/TableHoverActionsPlugin/index.tsx index 4a0c5cf5f..74336268e 100644 --- a/packages/richtext-lexical/src/features/experimental_table/client/plugins/TableHoverActionsPlugin/index.tsx +++ b/packages/richtext-lexical/src/features/experimental_table/client/plugins/TableHoverActionsPlugin/index.tsx @@ -16,7 +16,7 @@ import { } from '@lexical/table' import { $findMatchingParent, mergeRegister } from '@lexical/utils' import { $getNearestNodeFromDOMNode } from 'lexical' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import * as React from 'react' import { createPortal } from 'react-dom' @@ -25,15 +25,19 @@ import { useDebounce } from '../../utils/useDebounce.js' const BUTTON_WIDTH_PX = 20 -function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement }): JSX.Element { +function TableHoverActionsContainer({ + anchorElem, +}: { + anchorElem: HTMLElement +}): JSX.Element | null { 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 tableSetRef = useRef>(new Set()) + const tableCellDOMNodeRef = useRef(null) const debouncedOnMouseMove = useDebounce( (event: MouseEvent) => { @@ -49,7 +53,7 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement }) return } - tableDOMNodeRef.current = tableDOMNode + tableCellDOMNodeRef.current = tableDOMNode let hoveredRowNode: null | TableCellNode = null let hoveredColumnNode: null | TableCellNode = null @@ -88,9 +92,9 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement }) const { bottom: tableElemBottom, height: tableElemHeight, + left: tableElemLeft, right: tableElemRight, width: tableElemWidth, - x: tableElemX, y: tableElemY, } = (tableDOMElement as HTMLTableElement).getBoundingClientRect() @@ -101,7 +105,7 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement }) setShownRow(true) setPosition({ height: BUTTON_WIDTH_PX, - left: tableElemX - editorElemLeft, + left: tableElemLeft - editorElemLeft, top: tableElemBottom - editorElemY + 5, width: tableElemWidth, }) @@ -121,6 +125,15 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement }) 250, ) + // Hide the buttons on any table dimensions change to prevent last row cells + // overlap behind the 'Add Row' button when text entry changes cell height + const tableResizeObserver = useMemo(() => { + return new ResizeObserver(() => { + setShownRow(false) + setShownColumn(false) + }) + }, []) + useEffect(() => { if (!shouldListenMouseMove) { return @@ -143,15 +156,28 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement }) (mutations) => { editor.getEditorState().read(() => { for (const [key, type] of mutations) { + const tableDOMElement = editor.getElementByKey(key) + switch (type) { case 'created': - codeSetRef.current.add(key) - setShouldListenMouseMove(codeSetRef.current.size > 0) + tableSetRef.current.add(key) + setShouldListenMouseMove(tableSetRef.current.size > 0) + if (tableDOMElement) { + tableResizeObserver.observe(tableDOMElement) + } break case 'destroyed': - codeSetRef.current.delete(key) - setShouldListenMouseMove(codeSetRef.current.size > 0) + tableSetRef.current.delete(key) + setShouldListenMouseMove(tableSetRef.current.size > 0) + // Reset resize observers + tableResizeObserver.disconnect() + tableSetRef.current.forEach((tableKey: NodeKey) => { + const tableElement = editor.getElementByKey(tableKey) + if (tableElement) { + tableResizeObserver.observe(tableElement) + } + }) break default: @@ -163,12 +189,12 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement }) { skipInitialization: false }, ), ) - }, [editor]) + }, [editor, tableResizeObserver]) const insertAction = (insertRow: boolean) => { editor.update(() => { - if (tableDOMNodeRef.current) { - const maybeTableNode = $getNearestNodeFromDOMNode(tableDOMNodeRef.current) + if (tableCellDOMNodeRef.current) { + const maybeTableNode = $getNearestNodeFromDOMNode(tableCellDOMNodeRef.current) maybeTableNode?.selectEnd() if (insertRow) { $insertTableRow__EXPERIMENTAL() @@ -181,6 +207,10 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement }) }) } + if (!editor?.isEditable()) { + return null + } + return ( <> {isShownRow && ( @@ -233,5 +263,10 @@ export function TableHoverActionsPlugin({ }: { anchorElem?: HTMLElement }): null | React.ReactPortal { + const [editor] = useLexicalComposerContext() + if (!editor?.isEditable()) { + return null + } + return createPortal(, anchorElem) } diff --git a/packages/richtext-lexical/src/features/format/bold/feature.client.tsx b/packages/richtext-lexical/src/features/format/bold/feature.client.tsx index 6113a137b..f9ece7ff1 100644 --- a/packages/richtext-lexical/src/features/format/bold/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/bold/feature.client.tsx @@ -1,4 +1,5 @@ 'use client' +import { $isTableSelection } from '@lexical/table' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' @@ -18,7 +19,7 @@ const toolbarGroups: ToolbarGroup[] = [ { ChildComponent: BoldIcon, isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { + if ($isRangeSelection(selection) || $isTableSelection(selection)) { return selection.hasFormat('bold') } return false diff --git a/packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx b/packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx index 653d9b554..c520914c1 100644 --- a/packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx @@ -1,5 +1,6 @@ 'use client' +import { $isTableSelection } from '@lexical/table' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' @@ -14,7 +15,7 @@ const toolbarGroups: ToolbarGroup[] = [ { ChildComponent: CodeIcon, isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { + if ($isRangeSelection(selection) || $isTableSelection(selection)) { return selection.hasFormat('code') } return false diff --git a/packages/richtext-lexical/src/features/format/italic/feature.client.tsx b/packages/richtext-lexical/src/features/format/italic/feature.client.tsx index 5168e7495..53e7aae7c 100644 --- a/packages/richtext-lexical/src/features/format/italic/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/italic/feature.client.tsx @@ -1,5 +1,6 @@ 'use client' +import { $isTableSelection } from '@lexical/table' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' @@ -14,7 +15,7 @@ const toolbarGroups: ToolbarGroup[] = [ { ChildComponent: ItalicIcon, isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { + if ($isRangeSelection(selection) || $isTableSelection(selection)) { return selection.hasFormat('italic') } return false diff --git a/packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx b/packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx index 1e83749f8..8e0f87642 100644 --- a/packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx @@ -1,5 +1,6 @@ 'use client' +import { $isTableSelection } from '@lexical/table' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import { StrikethroughIcon } from '../../../lexical/ui/icons/Strikethrough/index.js' @@ -12,7 +13,7 @@ const toolbarGroups = [ { ChildComponent: StrikethroughIcon, isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { + if ($isRangeSelection(selection) || $isTableSelection(selection)) { return selection.hasFormat('strikethrough') } return false diff --git a/packages/richtext-lexical/src/features/format/subscript/feature.client.tsx b/packages/richtext-lexical/src/features/format/subscript/feature.client.tsx index b8617dd97..c4fe70013 100644 --- a/packages/richtext-lexical/src/features/format/subscript/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/subscript/feature.client.tsx @@ -1,5 +1,6 @@ 'use client' +import { $isTableSelection } from '@lexical/table' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' @@ -13,7 +14,7 @@ const toolbarGroups: ToolbarGroup[] = [ { ChildComponent: SubscriptIcon, isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { + if ($isRangeSelection(selection) || $isTableSelection(selection)) { return selection.hasFormat('subscript') } return false diff --git a/packages/richtext-lexical/src/features/format/superscript/feature.client.tsx b/packages/richtext-lexical/src/features/format/superscript/feature.client.tsx index e388c3d41..ccb82eef7 100644 --- a/packages/richtext-lexical/src/features/format/superscript/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/superscript/feature.client.tsx @@ -1,5 +1,6 @@ 'use client' +import { $isTableSelection } from '@lexical/table' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' @@ -13,7 +14,7 @@ const toolbarGroups: ToolbarGroup[] = [ { ChildComponent: SuperscriptIcon, isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { + if ($isRangeSelection(selection) || $isTableSelection(selection)) { return selection.hasFormat('superscript') } return false diff --git a/packages/richtext-lexical/src/features/format/underline/feature.client.tsx b/packages/richtext-lexical/src/features/format/underline/feature.client.tsx index f1c322c4f..cdacea8c7 100644 --- a/packages/richtext-lexical/src/features/format/underline/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/underline/feature.client.tsx @@ -1,5 +1,6 @@ 'use client' +import { $isTableSelection } from '@lexical/table' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' @@ -13,7 +14,7 @@ const toolbarGroups: ToolbarGroup[] = [ { ChildComponent: UnderlineIcon, isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { + if ($isRangeSelection(selection) || $isTableSelection(selection)) { return selection.hasFormat('underline') } return false diff --git a/packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx b/packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx index 589ab3078..b442780fc 100644 --- a/packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx +++ b/packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx @@ -20,6 +20,8 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr import type { MenuTextMatch } from '../useMenuTriggerMatch.js' import type { SlashMenuGroupInternal, SlashMenuItem, SlashMenuItemInternal } from './types.js' +import { CAN_USE_DOM } from '../../../utils/canUseDOM.js' + export type MenuResolution = { getRect: () => DOMRect match?: MenuTextMatch @@ -208,7 +210,7 @@ export function LexicalMenu({ resolution, shouldSplitNodeWithQuery = false, }: { - anchorElementRef: RefObject + anchorElementRef: RefObject close: () => void editor: LexicalEditor groups: Array @@ -432,10 +434,15 @@ export function useMenuAnchorRef( resolution: MenuResolution | null, setResolution: (r: MenuResolution | null) => void, className?: string, -): RefObject { +): RefObject { const [editor] = useLexicalComposerContext() - const anchorElementRef = useRef(document.createElement('div')) + const anchorElementRef = useRef( + CAN_USE_DOM ? document.createElement('div') : null, + ) const positionMenu = useCallback(() => { + if (anchorElementRef.current === null || parent === undefined) { + return + } const rootElement = editor.getRootElement() const containerDiv = anchorElementRef.current diff --git a/packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index.tsx b/packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index.tsx index 36ccb55db..736f47539 100644 --- a/packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index.tsx +++ b/packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index.tsx @@ -237,7 +237,7 @@ export function LexicalTypeaheadMenuPlugin({ } }, [editor, triggerFn, onQueryChange, resolution, closeTypeahead, openTypeahead]) - return resolution === null || editor === null ? null : ( + return anchorElementRef.current === null || resolution === null || editor === null ? null : (