feat(richtext-lexical): backport relevant from lexical playground between 0.18.0 and 0.20.0 (#9129)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user