@@ -255,6 +255,9 @@ export class SideMenuView<
255
255
private state ?: SideMenuState < BSchema , I , S > ;
256
256
private readonly emitUpdate : ( state : SideMenuState < BSchema , I , S > ) => void ;
257
257
258
+ private needUpdate = false ;
259
+ private mousePos : { x : number ; y : number } | undefined ;
260
+
258
261
// When true, the drag handle with be anchored at the same level as root elements
259
262
// When false, the drag handle with be just to the left of the element
260
263
// TODO: Is there any case where we want this to be false?
@@ -302,6 +305,78 @@ export class SideMenuView<
302
305
document . body . addEventListener ( "keydown" , this . onKeyDown , true ) ;
303
306
}
304
307
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
+
305
380
/**
306
381
* Sets isDragging when dragging text.
307
382
*/
@@ -390,25 +465,16 @@ export class SideMenuView<
390
465
} ;
391
466
392
467
onMouseMove = ( event : MouseEvent ) => {
393
- if ( this . menuFrozen ) {
394
- return ;
395
- }
468
+ this . mousePos = { x : event . clientX , y : event . clientY } ;
396
469
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 ( ) ;
404
470
// We want the full area of the editor to check if the cursor is hovering
405
471
// above it though.
406
472
const editorOuterBoundingBox = this . pmView . dom . getBoundingClientRect ( ) ;
407
473
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 ;
412
478
413
479
const editorWrapper = this . pmView . dom . parentElement ! ;
414
480
@@ -434,63 +500,11 @@ export class SideMenuView<
434
500
return ;
435
501
}
436
502
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 ( ) ;
445
504
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 ;
494
508
}
495
509
} ;
496
510
@@ -511,6 +525,24 @@ export class SideMenuView<
511
525
}
512
526
} ;
513
527
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
+
514
546
destroy ( ) {
515
547
if ( this . state ?. show ) {
516
548
this . state . show = false ;
0 commit comments