Skip to content

Commit 8784d15

Browse files
fix: Side menu not updating in edge cases (#791)
* Made side menu update on editor state update as well as mouse move * Added comment
1 parent c7954d9 commit 8784d15

File tree

1 file changed

+102
-70
lines changed

1 file changed

+102
-70
lines changed

packages/core/src/extensions/SideMenu/SideMenuPlugin.ts

Lines changed: 102 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)