11'use client' ;
22
33import { $findTableNode , $isTableSelection } from '@lexical/table' ;
4- import { Icon } from '@lobehub/ui' ;
5- import { Button , Dropdown , theme } from 'antd' ;
4+ import { DropdownMenu , type DropdownMenuProps , Icon , useAppElement } from '@lobehub/ui' ;
5+ import { Button , theme } from 'antd' ;
66import { cx } from 'antd-style' ;
77import { $getNodeByKey , $getSelection , $isRangeSelection } from 'lexical' ;
88import { GripVerticalIcon , PlusIcon } from 'lucide-react' ;
@@ -40,7 +40,7 @@ import {
4040 getTableBlockRect ,
4141 isTableBlockElement ,
4242} from './drag/drag-utils' ;
43- import { styles } from './style' ;
43+ import { ANCHOR_PADDING_CSS_VAR , styles } from './style' ;
4444
4545export interface ReactBlockPluginProps extends Omit < BlockPluginOptions , 'className' > {
4646 className ?: string ;
@@ -72,16 +72,22 @@ const getTableMenuAnchorRect = (element: HTMLElement) => {
7272const ReactBlockPlugin : FC < ReactBlockPluginProps > = ( props ) => {
7373 const { token } = theme . useToken ( ) ;
7474 const [ editor ] = useLexicalComposerContext ( ) ;
75+ const appElement = useAppElement ( ) ;
7576 const {
7677 rootClassName,
7778 className,
7879 attributeName,
80+ anchorPadding,
7981 locale,
8082 onHoverBlockChange,
8183 onDragTargetChange,
8284 onDragTargetResolve,
8385 } = props ;
8486 const mergedRootClassName = cx ( styles . root , rootClassName ?. trim ( ) || className ?. trim ( ) ) ;
87+ const anchorPaddingValue = useMemo ( ( ) => {
88+ if ( anchorPadding === undefined ) return null ;
89+ return typeof anchorPadding === 'number' ? `${ anchorPadding } px` : anchorPadding ;
90+ } , [ anchorPadding ] ) ;
8591 const menuRef = useRef < HTMLDivElement > ( null ) ;
8692 const dragLayerRef = useRef < HTMLDivElement > ( null ) ;
8793 const contextRef = useRef < RuntimeContextRef > ( createRuntimeContext ( ) ) ;
@@ -110,10 +116,27 @@ const ReactBlockPlugin: FC<ReactBlockPluginProps> = (props) => {
110116 }
111117
112118 editor . registerPlugin ( BlockPlugin , {
119+ anchorPadding,
113120 attributeName,
114121 className : mergedRootClassName ,
115122 } ) ;
116- } , [ attributeName , editor , locale , mergedRootClassName ] ) ;
123+ } , [ anchorPadding , attributeName , editor , locale , mergedRootClassName ] ) ;
124+
125+ useLexicalEditor (
126+ ( lexicalEditor ) =>
127+ lexicalEditor . registerRootListener ( ( rootElement , prevRootElement ) => {
128+ if ( prevRootElement ) {
129+ prevRootElement . style . removeProperty ( ANCHOR_PADDING_CSS_VAR ) ;
130+ }
131+ if ( ! rootElement ) return ;
132+ if ( anchorPaddingValue === null ) {
133+ rootElement . style . removeProperty ( ANCHOR_PADDING_CSS_VAR ) ;
134+ } else {
135+ rootElement . style . setProperty ( ANCHOR_PADDING_CSS_VAR , anchorPaddingValue ) ;
136+ }
137+ } ) ,
138+ [ anchorPaddingValue ] ,
139+ ) ;
117140
118141 useLexicalEditor ( ( ) => {
119142 const service = editor . requireService ( IBlockMenuService ) as BlockMenuService | null ;
@@ -625,8 +648,44 @@ const ReactBlockPlugin: FC<ReactBlockPluginProps> = (props) => {
625648 } ;
626649 } , [ blockMenuSuppressed , editor , isDragging ] ) ;
627650
651+ const menuContext = useMemo < IBlockMenuRenderContext | null > ( ( ) => {
652+ if ( operationMenuOpen && operationMenuContext ) {
653+ return operationMenuContext ;
654+ }
655+
656+ const lockedContext = blockMenuService ?. getMenuLockedContext ( ) ;
657+ if ( lockedContext ) {
658+ const root = editor . getRootElement ( ) ;
659+ const fresh = root ?. querySelector < HTMLElement > (
660+ `[data-block-id="${ CSS . escape ( lockedContext . blockId ) } "]` ,
661+ ) ;
662+ if ( fresh && root ?. contains ( fresh ) ) {
663+ return {
664+ blockElement : fresh ,
665+ blockId : lockedContext . blockId ,
666+ editor,
667+ } ;
668+ }
669+ }
670+
671+ if ( ! hoveredBlock ) return null ;
672+
673+ return {
674+ blockElement : hoveredBlock . blockElement ,
675+ blockId : hoveredBlock . blockId ,
676+ editor,
677+ } ;
678+ } , [
679+ editor ,
680+ hoveredBlock ,
681+ operationMenuOpen ,
682+ operationMenuContext ,
683+ blockMenuService ,
684+ menuVersion ,
685+ ] ) ;
686+
628687 useLayoutEffect ( ( ) => {
629- if ( ! hoveredBlock ) {
688+ if ( ! menuContext ) {
630689 if ( operationMenuOpen ) {
631690 return ;
632691 }
@@ -636,20 +695,20 @@ const ReactBlockPlugin: FC<ReactBlockPluginProps> = (props) => {
636695 }
637696
638697 const updateMenuPosition = ( ) => {
639- const blockRect = getBlockMeasureRect ( hoveredBlock . blockElement ) ;
698+ const blockRect = getBlockMeasureRect ( menuContext . blockElement ) ;
640699 if ( ! blockRect ) {
641700 setMenuPosition ( { } ) ;
642701 return ;
643702 }
644703
645704 const menuWidth = menuRef . current ?. offsetWidth || 32 ;
646705 const gap = 8 ;
647- const listItemOffset = hoveredBlock . blockElement . tagName === 'LI' ? 16 : 0 ;
648- const isTableBlock = isTableBlockElement ( hoveredBlock . blockElement ) ;
649- const isFocusedTableBlock = focusedTableBlockId === hoveredBlock . blockId && isTableBlock ;
706+ const listItemOffset = menuContext . blockElement . tagName === 'LI' ? 16 : 0 ;
707+ const isTableBlock = isTableBlockElement ( menuContext . blockElement ) ;
708+ const isFocusedTableBlock = focusedTableBlockId === menuContext . blockId && isTableBlock ;
650709 const tableMenuOffset = isFocusedTableBlock ? TABLE_FOCUSED_MENU_OFFSET : 0 ;
651710 const tableAnchorRect = isTableBlock
652- ? getTableMenuAnchorRect ( hoveredBlock . blockElement )
711+ ? getTableMenuAnchorRect ( menuContext . blockElement )
653712 : null ;
654713 const root = editor . getRootElement ( ) ;
655714 const rootRect = root ?. getBoundingClientRect ( ) ;
@@ -683,17 +742,7 @@ const ReactBlockPlugin: FC<ReactBlockPluginProps> = (props) => {
683742 window . removeEventListener ( 'resize' , updateMenuPosition ) ;
684743 document . removeEventListener ( 'scroll' , updateMenuPosition , true ) ;
685744 } ;
686- } , [ editor , focusedTableBlockId , hoveredBlock , layoutVersion , operationMenuOpen ] ) ;
687-
688- const menuContext = useMemo < IBlockMenuRenderContext | null > ( ( ) => {
689- if ( ! hoveredBlock ) return null ;
690-
691- return {
692- blockElement : hoveredBlock . blockElement ,
693- blockId : hoveredBlock . blockId ,
694- editor,
695- } ;
696- } , [ editor , hoveredBlock ] ) ;
745+ } , [ editor , focusedTableBlockId , menuContext , layoutVersion , operationMenuOpen ] ) ;
697746
698747 const operationMenus = useMemo ( ( ) => {
699748 if ( ! operationMenuContext || ! blockMenuService ) return [ ] ;
@@ -780,23 +829,6 @@ const ReactBlockPlugin: FC<ReactBlockPluginProps> = (props) => {
780829 setDragIndicator ( null ) ;
781830 } ;
782831
783- const toggleOperationMenu = ( context : IBlockMenuRenderContext | null ) => {
784- if ( ! context ) {
785- setOperationMenuOpen ( false ) ;
786- setOperationMenuContext ( null ) ;
787- return ;
788- }
789-
790- setOperationMenuOpen ( ( open ) => {
791- const shouldOpen = ! (
792- open && contextRef . current . operationMenuAnchorBlockId === context . blockId
793- ) ;
794- contextRef . current . operationMenuAnchorBlockId = shouldOpen ? context . blockId : null ;
795- setOperationMenuContext ( shouldOpen ? context : null ) ;
796- return shouldOpen ;
797- } ) ;
798- } ;
799-
800832 const handleDragHandlePointerDown = ( event : ReactPointerEvent < HTMLElement > ) => {
801833 if ( ! menuContext ) return ;
802834
@@ -815,20 +847,29 @@ const ReactBlockPlugin: FC<ReactBlockPluginProps> = (props) => {
815847 setDragIndicator,
816848 setOperationMenuContext,
817849 setOperationMenuOpen,
818- toggleOperationMenu,
819850 } ) ;
820851 } ;
821852
822- const handleDragHandleClick = ( ) => {
853+ const handleOperationMenuOpenChange = ( open : boolean ) => {
823854 if ( contextRef . current . ignoreNextHandleClick ) {
824855 contextRef . current . ignoreNextHandleClick = false ;
825856 return ;
826857 }
827858
828- toggleOperationMenu ( menuContext ) ;
859+ if ( ! menuContext ) return ;
860+
861+ if ( open ) {
862+ setOperationMenuOpen ( true ) ;
863+ setOperationMenuContext ( menuContext ) ;
864+ contextRef . current . operationMenuAnchorBlockId = menuContext . blockId ;
865+ } else {
866+ setOperationMenuOpen ( false ) ;
867+ setOperationMenuContext ( null ) ;
868+ contextRef . current . operationMenuAnchorBlockId = null ;
869+ }
829870 } ;
830871
831- const dropdownItems = useMemo (
872+ const dropdownItems = useMemo < DropdownMenuProps [ 'items' ] > (
832873 ( ) =>
833874 operationMenus . map ( ( item ) => ( {
834875 key : item . key ,
@@ -864,36 +905,25 @@ const ReactBlockPlugin: FC<ReactBlockPluginProps> = (props) => {
864905 />
865906 ) ;
866907 } ) }
867- < Dropdown
868- align = { {
869- points : [ 'tr' , 'tl' ] ,
870- } }
871- classNames = { {
872- root : OPERATION_MENU_OVERLAY_CLASS ,
873- } }
874- menu = { { items : dropdownItems } }
875- onOpenChange = { ( open ) => {
876- if ( ! open ) {
877- setOperationMenuOpen ( false ) ;
878- setOperationMenuContext ( null ) ;
879- contextRef . current . operationMenuAnchorBlockId = null ;
880- }
881- } }
908+ < DropdownMenu
909+ items = { dropdownItems }
910+ onOpenChange = { handleOperationMenuOpenChange }
882911 open = { operationMenuOpen && operationMenuContext ?. blockId === menuContext . blockId }
883- trigger = { [ ] }
912+ placement = { 'leftTop' }
913+ popupProps = { { className : OPERATION_MENU_OVERLAY_CLASS } }
914+ positionerProps = { { style : { zIndex : 1000 } } }
884915 >
885916 < Button
886917 aria-label = { 'Block actions and drag' }
887918 className = { styles . dragHandle }
888919 data-block-drag-handle = { 'true' }
889920 icon = { < Icon icon = { GripVerticalIcon } size = { 14 } /> }
890- onClick = { handleDragHandleClick }
891921 onPointerDown = { handleDragHandlePointerDown }
892922 size = { 'small' }
893923 title = { 'Block actions and drag' }
894924 type = { 'text' }
895925 />
896- </ Dropdown >
926+ </ DropdownMenu >
897927 </ div >
898928 </ div >
899929 ) : null ;
@@ -925,7 +955,7 @@ const ReactBlockPlugin: FC<ReactBlockPluginProps> = (props) => {
925955 />
926956 ) }
927957 </ > ,
928- document . body ,
958+ appElement ?? document . body ,
929959 ) }
930960 </ >
931961 ) ;
0 commit comments