Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 102 additions & 70 deletions packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ export class SideMenuView<
private state?: SideMenuState<BSchema, I, S>;
private readonly emitUpdate: (state: SideMenuState<BSchema, I, S>) => void;

private needUpdate = false;
private mousePos: { x: number; y: number } | undefined;

// When true, the drag handle with be anchored at the same level as root elements
// When false, the drag handle with be just to the left of the element
// TODO: Is there any case where we want this to be false?
Expand Down Expand Up @@ -302,6 +305,78 @@ export class SideMenuView<
document.body.addEventListener("keydown", this.onKeyDown, true);
}

updateState = () => {
if (this.menuFrozen || !this.mousePos) {
return;
}

// Editor itself may have padding or other styling which affects
// size/position, so we get the boundingRect of the first child (i.e. the
// blockGroup that wraps all blocks in the editor) for more accurate side
// menu placement.
const editorBoundingBox = (
this.pmView.dom.firstChild! as HTMLElement
).getBoundingClientRect();

this.horizontalPosAnchor = editorBoundingBox.x;

// Gets block at mouse cursor's vertical position.
const coords = {
left: editorBoundingBox.left + editorBoundingBox.width / 2, // take middle of editor
top: this.mousePos.y,
};
const block = getDraggableBlockFromCoords(coords, this.pmView);

// Closes the menu if the mouse cursor is beyond the editor vertically.
if (!block || !this.editor.isEditable) {
if (this.state?.show) {
this.state.show = false;
this.needUpdate = true;
}

return;
}

// Doesn't update if the menu is already open and the mouse cursor is still hovering the same block.
if (
this.state?.show &&
this.hoveredBlock?.hasAttribute("data-id") &&
this.hoveredBlock?.getAttribute("data-id") === block.id
) {
return;
}

this.hoveredBlock = block.node;

// Gets the block's content node, which lets to ignore child blocks when determining the block menu's position.
const blockContent = block.node.firstChild as HTMLElement;

if (!blockContent) {
return;
}

// Shows or updates elements.
if (this.editor.isEditable) {
const blockContentBoundingBox = blockContent.getBoundingClientRect();

this.state = {
show: true,
referencePos: new DOMRect(
this.horizontalPosAnchoredAtRoot
? this.horizontalPosAnchor
: blockContentBoundingBox.x,
blockContentBoundingBox.y,
blockContentBoundingBox.width,
blockContentBoundingBox.height
),
block: this.editor.getBlock(
this.hoveredBlock!.getAttribute("data-id")!
)!,
};
this.needUpdate = true;
}
};

/**
* Sets isDragging when dragging text.
*/
Expand Down Expand Up @@ -390,25 +465,16 @@ export class SideMenuView<
};

onMouseMove = (event: MouseEvent) => {
if (this.menuFrozen) {
return;
}
this.mousePos = { x: event.clientX, y: event.clientY };

// Editor itself may have padding or other styling which affects
// size/position, so we get the boundingRect of the first child (i.e. the
// blockGroup that wraps all blocks in the editor) for more accurate side
// menu placement.
const editorBoundingBox = (
this.pmView.dom.firstChild! as HTMLElement
).getBoundingClientRect();
// We want the full area of the editor to check if the cursor is hovering
// above it though.
const editorOuterBoundingBox = this.pmView.dom.getBoundingClientRect();
const cursorWithinEditor =
event.clientX >= editorOuterBoundingBox.left &&
event.clientX <= editorOuterBoundingBox.right &&
event.clientY >= editorOuterBoundingBox.top &&
event.clientY <= editorOuterBoundingBox.bottom;
this.mousePos.x > editorOuterBoundingBox.left &&
this.mousePos.x < editorOuterBoundingBox.right &&
this.mousePos.y > editorOuterBoundingBox.top &&
this.mousePos.y < editorOuterBoundingBox.bottom;

const editorWrapper = this.pmView.dom.parentElement!;

Expand All @@ -434,63 +500,11 @@ export class SideMenuView<
return;
}

this.horizontalPosAnchor = editorBoundingBox.x;

// Gets block at mouse cursor's vertical position.
const coords = {
left: editorBoundingBox.left + editorBoundingBox.width / 2, // take middle of editor
top: event.clientY,
};
const block = getDraggableBlockFromCoords(coords, this.pmView);
this.updateState();

// Closes the menu if the mouse cursor is beyond the editor vertically.
if (!block || !this.editor.isEditable) {
if (this.state?.show) {
this.state.show = false;
this.emitUpdate(this.state);
}

return;
}

// Doesn't update if the menu is already open and the mouse cursor is still hovering the same block.
if (
this.state?.show &&
this.hoveredBlock?.hasAttribute("data-id") &&
this.hoveredBlock?.getAttribute("data-id") === block.id
) {
return;
}

this.hoveredBlock = block.node;

// Gets the block's content node, which lets to ignore child blocks when determining the block menu's position.
const blockContent = block.node.firstChild as HTMLElement;

if (!blockContent) {
return;
}

// Shows or updates elements.
if (this.editor.isEditable) {
const blockContentBoundingBox = blockContent.getBoundingClientRect();

this.state = {
show: true,
referencePos: new DOMRect(
this.horizontalPosAnchoredAtRoot
? this.horizontalPosAnchor
: blockContentBoundingBox.x,
blockContentBoundingBox.y,
blockContentBoundingBox.width,
blockContentBoundingBox.height
),
block: this.editor.getBlock(
this.hoveredBlock!.getAttribute("data-id")!
)!,
};

this.emitUpdate(this.state);
if (this.needUpdate) {
this.emitUpdate(this.state!);
this.needUpdate = false;
}
};

Expand All @@ -511,6 +525,24 @@ export class SideMenuView<
}
};

// Needed in cases where the editor state updates without the mouse cursor
// moving, as some state updates can require a side menu update. For example,
// adding a button to the side menu which removes the block can cause the
// block below to jump up into the place of the removed block when clicked,
// allowing the user to click the button again without moving the cursor. This
// would otherwise not update the side menu, and so clicking the button again
// would attempt to remove the same block again, causing an error.
update() {
const prevBlockId = this.state?.block.id;

this.updateState();

if (this.needUpdate && this.state && prevBlockId !== this.state.block.id) {
this.emitUpdate(this.state);
this.needUpdate = false;
}
}

destroy() {
if (this.state?.show) {
this.state.show = false;
Expand Down