1
1
'use client'
2
2
3
- import type {
4
- HTMLTableElementWithWithTableSelectionState ,
5
- TableRowNode ,
6
- TableSelection ,
7
- } from '@lexical/table'
3
+ import type { TableObserver , TableRowNode , TableSelection } from '@lexical/table'
8
4
import type { ElementNode } from 'lexical'
9
5
import type { JSX } from 'react'
10
6
@@ -24,10 +20,12 @@ import {
24
20
$isTableRowNode ,
25
21
$isTableSelection ,
26
22
$unmergeCell ,
23
+ getTableElement ,
27
24
getTableObserverFromTableElement ,
28
25
TableCellHeaderStates ,
29
26
TableCellNode ,
30
27
} from '@lexical/table'
28
+ import { mergeRegister } from '@lexical/utils'
31
29
import { useScrollInfo } from '@payloadcms/ui'
32
30
import {
33
31
$createParagraphNode ,
@@ -37,15 +35,18 @@ import {
37
35
$isParagraphNode ,
38
36
$isRangeSelection ,
39
37
$isTextNode ,
38
+ COMMAND_PRIORITY_CRITICAL ,
39
+ getDOMSelection ,
40
+ SELECTION_CHANGE_COMMAND ,
40
41
} from 'lexical'
41
42
import * as React from 'react'
42
43
import { useCallback , useEffect , useRef , useState } from 'react'
43
44
import { createPortal } from 'react-dom'
44
45
45
46
import type { PluginComponentWithAnchor } from '../../../../typesClient.js'
46
47
47
- import { MeatballsIcon } from '../../../../../lexical/ui/icons/Meatballs/index.js'
48
48
import './index.scss'
49
+ import { MeatballsIcon } from '../../../../../lexical/ui/icons/Meatballs/index.js'
49
50
50
51
function computeSelectionCount ( selection : TableSelection ) : {
51
52
columns : number
@@ -201,17 +202,15 @@ function TableActionMenu({
201
202
editor . update ( ( ) => {
202
203
if ( tableCellNode . isAttached ( ) ) {
203
204
const tableNode = $getTableNodeFromLexicalNodeOrThrow ( tableCellNode )
204
- const tableElement = editor . getElementByKey (
205
- tableNode . getKey ( ) ,
206
- ) as HTMLTableElementWithWithTableSelectionState
205
+ const tableElement = getTableElement ( tableNode , editor . getElementByKey ( tableNode . getKey ( ) ) )
207
206
208
- if ( ! tableElement ) {
207
+ if ( tableElement === null ) {
209
208
throw new Error ( 'Expected to find tableElement in DOM' )
210
209
}
211
210
212
211
const tableObserver = getTableObserverFromTableElement ( tableElement )
213
212
if ( tableObserver !== null ) {
214
- tableObserver . clearHighlight ( )
213
+ tableObserver . $ clearHighlight( )
215
214
}
216
215
217
216
tableNode . markDirty ( )
@@ -409,7 +408,7 @@ function TableActionMenu({
409
408
onClick = { ( ) => mergeTableCellsAtSelection ( ) }
410
409
type = "button"
411
410
>
412
- Merge cells
411
+ < span className = "text" > Merge cells</ span >
413
412
</ button >
414
413
)
415
414
} else if ( canUnmergeCell ) {
@@ -420,7 +419,7 @@ function TableActionMenu({
420
419
onClick = { ( ) => unmergeTableCellsAtSelection ( ) }
421
420
type = "button"
422
421
>
423
- Unmerge cells
422
+ < span className = "text" > Unmerge cells</ span >
424
423
</ button >
425
424
)
426
425
}
@@ -555,24 +554,32 @@ function TableCellActionMenuContainer({
555
554
} ) : JSX . Element {
556
555
const [ editor ] = useLexicalComposerContext ( )
557
556
558
- const menuButtonRef = useRef ( null )
559
- const menuRootRef = useRef ( null )
557
+ const menuButtonRef = useRef < HTMLDivElement | null > ( null )
558
+ const menuRootRef = useRef < HTMLButtonElement | null > ( null )
560
559
const [ isMenuOpen , setIsMenuOpen ] = useState ( false )
561
560
562
561
const [ tableCellNode , setTableMenuCellNode ] = useState < null | TableCellNode > ( null )
563
562
564
563
const $moveMenu = useCallback ( ( ) => {
565
564
const menu = menuButtonRef . current
566
565
const selection = $getSelection ( )
567
- const nativeSelection = window . getSelection ( )
566
+ const nativeSelection = getDOMSelection ( editor . _window )
568
567
const activeElement = document . activeElement
568
+ function disable ( ) {
569
+ if ( menu ) {
570
+ menu . classList . remove ( 'table-cell-action-button-container--active' )
571
+ menu . classList . add ( 'table-cell-action-button-container--inactive' )
572
+ }
573
+ setTableMenuCellNode ( null )
574
+ }
569
575
570
576
if ( selection == null || menu == null ) {
571
- setTableMenuCellNode ( null )
572
- return
577
+ return disable ( )
573
578
}
574
579
575
580
const rootElement = editor . getRootElement ( )
581
+ let tableObserver : null | TableObserver = null
582
+ let tableCellParentNodeDOM : HTMLElement | null = null
576
583
577
584
if (
578
585
$isRangeSelection ( selection ) &&
@@ -585,53 +592,85 @@ function TableCellActionMenuContainer({
585
592
)
586
593
587
594
if ( tableCellNodeFromSelection == null ) {
588
- setTableMenuCellNode ( null )
589
- return
595
+ return disable ( )
590
596
}
591
597
592
- const tableCellParentNodeDOM = editor . getElementByKey ( tableCellNodeFromSelection . getKey ( ) )
598
+ tableCellParentNodeDOM = editor . getElementByKey ( tableCellNodeFromSelection . getKey ( ) )
593
599
594
- if ( tableCellParentNodeDOM == null ) {
595
- setTableMenuCellNode ( null )
596
- return
600
+ if ( tableCellParentNodeDOM == null || ! tableCellNodeFromSelection . isAttached ( ) ) {
601
+ return disable ( )
597
602
}
598
603
599
- setTableMenuCellNode ( tableCellNodeFromSelection )
600
- } else if ( ! activeElement ) {
601
- setTableMenuCellNode ( null )
602
- }
603
- } , [ editor ] )
604
-
605
- useEffect ( ( ) => {
606
- return editor . registerUpdateListener ( ( ) => {
607
- editor . getEditorState ( ) . read ( ( ) => {
608
- $moveMenu ( )
609
- } )
610
- } )
611
- } )
604
+ const tableNode = $getTableNodeFromLexicalNodeOrThrow ( tableCellNodeFromSelection )
605
+ const tableElement = getTableElement ( tableNode , editor . getElementByKey ( tableNode . getKey ( ) ) )
612
606
613
- useEffect ( ( ) => {
614
- const menuButtonDOM = menuButtonRef . current as HTMLButtonElement | null
607
+ if ( tableElement === null ) {
608
+ throw new Error ( 'TableActionMenu: Expected to find tableElement in DOM' )
609
+ }
615
610
616
- if ( menuButtonDOM != null && tableCellNode != null ) {
617
- const tableCellNodeDOM = editor . getElementByKey ( tableCellNode . getKey ( ) )
611
+ tableObserver = getTableObserverFromTableElement ( tableElement )
612
+ setTableMenuCellNode ( tableCellNodeFromSelection )
613
+ } else if ( $isTableSelection ( selection ) ) {
614
+ const anchorNode = $getTableCellNodeFromLexicalNode ( selection . anchor . getNode ( ) )
615
+ if ( ! $isTableCellNode ( anchorNode ) ) {
616
+ throw new Error ( 'TableSelection anchorNode must be a TableCellNode' )
617
+ }
618
+ const tableNode = $getTableNodeFromLexicalNodeOrThrow ( anchorNode )
619
+ const tableElement = getTableElement ( tableNode , editor . getElementByKey ( tableNode . getKey ( ) ) )
618
620
619
- if ( tableCellNodeDOM != null ) {
620
- const tableCellRect = tableCellNodeDOM . getBoundingClientRect ( )
621
- const menuRect = menuButtonDOM . getBoundingClientRect ( )
622
- const anchorRect = anchorElem . getBoundingClientRect ( )
621
+ if ( tableElement === null ) {
622
+ throw new Error ( 'TableActionMenu: Expected to find tableElement in DOM' )
623
+ }
623
624
624
- const top = tableCellRect . top - anchorRect . top + 4
625
- const left = tableCellRect . right - menuRect . width - 10 - anchorRect . left
625
+ tableObserver = getTableObserverFromTableElement ( tableElement )
626
+ tableCellParentNodeDOM = editor . getElementByKey ( anchorNode . getKey ( ) )
627
+ } else if ( ! activeElement ) {
628
+ return disable ( )
629
+ }
630
+ if ( tableObserver === null || tableCellParentNodeDOM === null ) {
631
+ return disable ( )
632
+ }
633
+ const enabled = ! tableObserver || ! tableObserver . isSelecting
634
+ menu . classList . toggle ( 'table-cell-action-button-container--active' , enabled )
635
+ menu . classList . toggle ( 'table-cell-action-button-container--inactive' , ! enabled )
636
+ if ( enabled ) {
637
+ const tableCellRect = tableCellParentNodeDOM . getBoundingClientRect ( )
638
+ const anchorRect = anchorElem . getBoundingClientRect ( )
639
+ const top = tableCellRect . top - anchorRect . top
640
+ const left = tableCellRect . right - anchorRect . left
641
+ menu . style . transform = `translate(${ left } px, ${ top } px)`
642
+ }
643
+ } , [ editor , anchorElem ] )
626
644
627
- menuButtonDOM . style . opacity = '1'
628
- menuButtonDOM . style . transform = `translate(${ left } px, ${ top } px)`
629
- } else {
630
- menuButtonDOM . style . opacity = '0'
631
- menuButtonDOM . style . transform = 'translate(-10000px, -10000px)'
645
+ useEffect ( ( ) => {
646
+ // We call the $moveMenu callback every time the selection changes,
647
+ // once up front, and once after each mouseUp
648
+ let timeoutId : ReturnType < typeof setTimeout > | undefined = undefined
649
+ const callback = ( ) => {
650
+ timeoutId = undefined
651
+ editor . getEditorState ( ) . read ( $moveMenu )
652
+ }
653
+ const delayedCallback = ( ) => {
654
+ if ( timeoutId === undefined ) {
655
+ timeoutId = setTimeout ( callback , 0 )
632
656
}
657
+ return false
633
658
}
634
- } , [ menuButtonRef , tableCellNode , editor , anchorElem ] )
659
+ return mergeRegister (
660
+ editor . registerUpdateListener ( delayedCallback ) ,
661
+ editor . registerCommand ( SELECTION_CHANGE_COMMAND , delayedCallback , COMMAND_PRIORITY_CRITICAL ) ,
662
+ editor . registerRootListener ( ( rootElement , prevRootElement ) => {
663
+ if ( prevRootElement ) {
664
+ prevRootElement . removeEventListener ( 'mouseup' , delayedCallback )
665
+ }
666
+ if ( rootElement ) {
667
+ rootElement . addEventListener ( 'mouseup' , delayedCallback )
668
+ delayedCallback ( )
669
+ }
670
+ } ) ,
671
+ ( ) => clearTimeout ( timeoutId ) ,
672
+ )
673
+ } )
635
674
636
675
const prevTableCellDOM = useRef ( tableCellNode )
637
676
0 commit comments