@@ -255,6 +255,9 @@ export class SideMenuView<
255255 private state ?: SideMenuState < BSchema , I , S > ;
256256 private readonly emitUpdate : ( state : SideMenuState < BSchema , I , S > ) => void ;
257257
258+ private needUpdate = false ;
259+ private mousePos : { x : number ; y : number } | undefined ;
260+
258261 // When true, the drag handle with be anchored at the same level as root elements
259262 // When false, the drag handle with be just to the left of the element
260263 // TODO: Is there any case where we want this to be false?
@@ -302,6 +305,78 @@ export class SideMenuView<
302305 document . body . addEventListener ( "keydown" , this . onKeyDown , true ) ;
303306 }
304307
308+ updateState = ( ) => {
309+ if ( this . menuFrozen || ! this . mousePos ) {
310+ return ;
311+ }
312+
313+ // Editor itself may have padding or other styling which affects
314+ // size/position, so we get the boundingRect of the first child (i.e. the
315+ // blockGroup that wraps all blocks in the editor) for more accurate side
316+ // menu placement.
317+ const editorBoundingBox = (
318+ this . pmView . dom . firstChild ! as HTMLElement
319+ ) . getBoundingClientRect ( ) ;
320+
321+ this . horizontalPosAnchor = editorBoundingBox . x ;
322+
323+ // Gets block at mouse cursor's vertical position.
324+ const coords = {
325+ left : editorBoundingBox . left + editorBoundingBox . width / 2 , // take middle of editor
326+ top : this . mousePos . y ,
327+ } ;
328+ const block = getDraggableBlockFromCoords ( coords , this . pmView ) ;
329+
330+ // Closes the menu if the mouse cursor is beyond the editor vertically.
331+ if ( ! block || ! this . editor . isEditable ) {
332+ if ( this . state ?. show ) {
333+ this . state . show = false ;
334+ this . needUpdate = true ;
335+ }
336+
337+ return ;
338+ }
339+
340+ // Doesn't update if the menu is already open and the mouse cursor is still hovering the same block.
341+ if (
342+ this . state ?. show &&
343+ this . hoveredBlock ?. hasAttribute ( "data-id" ) &&
344+ this . hoveredBlock ?. getAttribute ( "data-id" ) === block . id
345+ ) {
346+ return ;
347+ }
348+
349+ this . hoveredBlock = block . node ;
350+
351+ // Gets the block's content node, which lets to ignore child blocks when determining the block menu's position.
352+ const blockContent = block . node . firstChild as HTMLElement ;
353+
354+ if ( ! blockContent ) {
355+ return ;
356+ }
357+
358+ // Shows or updates elements.
359+ if ( this . editor . isEditable ) {
360+ const blockContentBoundingBox = blockContent . getBoundingClientRect ( ) ;
361+
362+ this . state = {
363+ show : true ,
364+ referencePos : new DOMRect (
365+ this . horizontalPosAnchoredAtRoot
366+ ? this . horizontalPosAnchor
367+ : blockContentBoundingBox . x ,
368+ blockContentBoundingBox . y ,
369+ blockContentBoundingBox . width ,
370+ blockContentBoundingBox . height
371+ ) ,
372+ block : this . editor . getBlock (
373+ this . hoveredBlock ! . getAttribute ( "data-id" ) !
374+ ) ! ,
375+ } ;
376+ this . needUpdate = true ;
377+ }
378+ } ;
379+
305380 /**
306381 * Sets isDragging when dragging text.
307382 */
@@ -390,25 +465,16 @@ export class SideMenuView<
390465 } ;
391466
392467 onMouseMove = ( event : MouseEvent ) => {
393- if ( this . menuFrozen ) {
394- return ;
395- }
468+ this . mousePos = { x : event . clientX , y : event . clientY } ;
396469
397- // Editor itself may have padding or other styling which affects
398- // size/position, so we get the boundingRect of the first child (i.e. the
399- // blockGroup that wraps all blocks in the editor) for more accurate side
400- // menu placement.
401- const editorBoundingBox = (
402- this . pmView . dom . firstChild ! as HTMLElement
403- ) . getBoundingClientRect ( ) ;
404470 // We want the full area of the editor to check if the cursor is hovering
405471 // above it though.
406472 const editorOuterBoundingBox = this . pmView . dom . getBoundingClientRect ( ) ;
407473 const cursorWithinEditor =
408- event . clientX >= editorOuterBoundingBox . left &&
409- event . clientX <= editorOuterBoundingBox . right &&
410- event . clientY >= editorOuterBoundingBox . top &&
411- event . clientY <= editorOuterBoundingBox . bottom ;
474+ this . mousePos . x > editorOuterBoundingBox . left &&
475+ this . mousePos . x < editorOuterBoundingBox . right &&
476+ this . mousePos . y > editorOuterBoundingBox . top &&
477+ this . mousePos . y < editorOuterBoundingBox . bottom ;
412478
413479 const editorWrapper = this . pmView . dom . parentElement ! ;
414480
@@ -434,63 +500,11 @@ export class SideMenuView<
434500 return ;
435501 }
436502
437- this . horizontalPosAnchor = editorBoundingBox . x ;
438-
439- // Gets block at mouse cursor's vertical position.
440- const coords = {
441- left : editorBoundingBox . left + editorBoundingBox . width / 2 , // take middle of editor
442- top : event . clientY ,
443- } ;
444- const block = getDraggableBlockFromCoords ( coords , this . pmView ) ;
503+ this . updateState ( ) ;
445504
446- // Closes the menu if the mouse cursor is beyond the editor vertically.
447- if ( ! block || ! this . editor . isEditable ) {
448- if ( this . state ?. show ) {
449- this . state . show = false ;
450- this . emitUpdate ( this . state ) ;
451- }
452-
453- return ;
454- }
455-
456- // Doesn't update if the menu is already open and the mouse cursor is still hovering the same block.
457- if (
458- this . state ?. show &&
459- this . hoveredBlock ?. hasAttribute ( "data-id" ) &&
460- this . hoveredBlock ?. getAttribute ( "data-id" ) === block . id
461- ) {
462- return ;
463- }
464-
465- this . hoveredBlock = block . node ;
466-
467- // Gets the block's content node, which lets to ignore child blocks when determining the block menu's position.
468- const blockContent = block . node . firstChild as HTMLElement ;
469-
470- if ( ! blockContent ) {
471- return ;
472- }
473-
474- // Shows or updates elements.
475- if ( this . editor . isEditable ) {
476- const blockContentBoundingBox = blockContent . getBoundingClientRect ( ) ;
477-
478- this . state = {
479- show : true ,
480- referencePos : new DOMRect (
481- this . horizontalPosAnchoredAtRoot
482- ? this . horizontalPosAnchor
483- : blockContentBoundingBox . x ,
484- blockContentBoundingBox . y ,
485- blockContentBoundingBox . width ,
486- blockContentBoundingBox . height
487- ) ,
488- block : this . editor . getBlock (
489- this . hoveredBlock ! . getAttribute ( "data-id" ) !
490- ) ! ,
491- } ;
492-
493- this . emitUpdate ( this . state ) ;
505+ if ( this . needUpdate ) {
506+ this . emitUpdate ( this . state ! ) ;
507+ this . needUpdate = false ;
494508 }
495509 } ;
496510
@@ -511,6 +525,24 @@ export class SideMenuView<
511525 }
512526 } ;
513527
528+ // Needed in cases where the editor state updates without the mouse cursor
529+ // moving, as some state updates can require a side menu update. For example,
530+ // adding a button to the side menu which removes the block can cause the
531+ // block below to jump up into the place of the removed block when clicked,
532+ // allowing the user to click the button again without moving the cursor. This
533+ // would otherwise not update the side menu, and so clicking the button again
534+ // would attempt to remove the same block again, causing an error.
535+ update ( ) {
536+ const prevBlockId = this . state ?. block . id ;
537+
538+ this . updateState ( ) ;
539+
540+ if ( this . needUpdate && this . state && prevBlockId !== this . state . block . id ) {
541+ this . emitUpdate ( this . state ) ;
542+ this . needUpdate = false ;
543+ }
544+ }
545+
514546 destroy ( ) {
515547 if ( this . state ?. show ) {
516548 this . state . show = false ;
0 commit comments