feat(richtext-lexical): backport relevant from lexical playground between 0.18.0 and 0.20.0 (#9129)

This commit is contained in:
Alessio Gravili
2024-11-11 21:58:28 -07:00
committed by GitHub
parent df764dbbef
commit a30eeaf644
10 changed files with 74 additions and 25 deletions

View File

@@ -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<boolean>(false)
const [isShownColumn, setShownColumn] = useState<boolean>(false)
const [shouldListenMouseMove, setShouldListenMouseMove] = useState<boolean>(false)
const [position, setPosition] = useState({})
const codeSetRef = useRef<Set<NodeKey>>(new Set())
const tableDOMNodeRef = useRef<HTMLElement | null>(null)
const tableSetRef = useRef<Set<NodeKey>>(new Set())
const tableCellDOMNodeRef = useRef<HTMLElement | null>(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(<TableHoverActionsContainer anchorElem={anchorElem} />, anchorElem)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<HTMLElement>
anchorElementRef: RefObject<HTMLElement | null>
close: () => void
editor: LexicalEditor
groups: Array<SlashMenuGroupInternal>
@@ -432,10 +434,15 @@ export function useMenuAnchorRef(
resolution: MenuResolution | null,
setResolution: (r: MenuResolution | null) => void,
className?: string,
): RefObject<HTMLElement> {
): RefObject<HTMLElement | null> {
const [editor] = useLexicalComposerContext()
const anchorElementRef = useRef<HTMLElement>(document.createElement('div'))
const anchorElementRef = useRef<HTMLElement | null>(
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

View File

@@ -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 : (
<LexicalMenu
anchorElementRef={anchorElementRef}
close={closeTypeahead}