@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
196 lines (174 loc) • 5.18 kB
text/typescript
import type { EditorView } from "prosemirror-view";
/**
* The orientation of the drop cursor.
*/
export type DropCursorOrientation =
| "inline" // Vertical line within text
| "block-horizontal" // Horizontal line between blocks
| "block-vertical-left" // Vertical line on left edge of block
| "block-vertical-right"; // Vertical line on right edge of block
/**
* The position and orientation of the drop cursor.
*/
export type DropCursorPosition = {
pos: number; // Document position
orientation: DropCursorOrientation;
};
/**
* Bounding rectangle in viewport coordinates (e.g. from getBoundingClientRect).
*/
export type Rect = {
left: number;
right: number;
top: number;
bottom: number;
};
/**
* Returns true if the element or any ancestor has the given CSS class.
* Used to skip drop cursor for elements marked with the exclusion class (e.g. drag handles).
*/
export function hasExclusionClassname(
element: Element | null,
exclude: string,
): boolean {
if (!element || !exclude) {
return false;
}
return !!element.closest(`.${exclude}`);
}
/**
* Computes the viewport rect for a block-level drop cursor (horizontal line between blocks
* or vertical line on left/right edge). Returns null for inline positions or when no DOM node exists.
*/
export function getBlockDropRect(
view: EditorView,
cursorPos: DropCursorPosition,
width: number,
scaleX: number,
scaleY: number,
): Rect | null {
const $pos = view.state.doc.resolve(cursorPos.pos);
const isBlock = !$pos.parent.inlineContent;
if (!isBlock || cursorPos.orientation === "inline") {
return null;
}
const before = $pos.nodeBefore;
const after = $pos.nodeAfter;
if (!before && !after) {
return null;
}
const isVertical =
cursorPos.orientation === "block-vertical-left" ||
cursorPos.orientation === "block-vertical-right";
// For vertical cursors, position is at the node position, for horizontal cursors, position is at the node before position
const nodePos = isVertical
? cursorPos.pos
: cursorPos.pos - (before ? before.nodeSize : 0);
const node = view.nodeDOM(nodePos) as HTMLElement | null;
if (!node) {
return null;
}
const nodeRect = node.getBoundingClientRect();
if (isVertical) {
const halfWidth = (width / 2) * scaleX;
const left =
cursorPos.orientation === "block-vertical-left"
? nodeRect.left
: nodeRect.right;
return {
left: left - halfWidth,
right: left + halfWidth,
top: nodeRect.top,
bottom: nodeRect.bottom,
};
}
let top = before ? nodeRect.bottom : nodeRect.top;
if (before && after) {
top =
(top +
(view.nodeDOM(cursorPos.pos) as HTMLElement).getBoundingClientRect()
.top) /
2;
}
const halfHeight = (width / 2) * scaleY;
return {
left: nodeRect.left,
right: nodeRect.right,
top: top - halfHeight,
bottom: top + halfHeight,
};
}
/**
* Computes the viewport rect for an inline drop cursor (vertical line within text).
*/
export function getInlineDropRect(
view: EditorView,
cursorPos: DropCursorPosition,
width: number,
scaleX: number,
): Rect {
const coords = view.coordsAtPos(cursorPos.pos);
const halfWidth = (width / 2) * scaleX;
return {
left: coords.left - halfWidth,
right: coords.left + halfWidth,
top: coords.top,
bottom: coords.bottom,
};
}
/**
* Applies orientation-specific CSS classes to the drop cursor element so it can be
* styled correctly (e.g. horizontal vs vertical line, inline vs block).
*/
export function applyOrientationClasses(
el: HTMLElement,
orientation: DropCursorOrientation,
) {
el.classList.toggle(
"prosemirror-dropcursor-inline",
orientation === "inline",
);
el.classList.toggle(
"prosemirror-dropcursor-block-horizontal",
orientation === "block-horizontal",
);
el.classList.toggle(
"prosemirror-dropcursor-block-vertical-left",
orientation === "block-vertical-left",
);
el.classList.toggle(
"prosemirror-dropcursor-block-vertical-right",
orientation === "block-vertical-right",
);
el.classList.toggle(
"prosemirror-dropcursor-block",
orientation === "block-horizontal",
);
el.classList.toggle(
"prosemirror-dropcursor-vertical",
orientation === "block-vertical-left" ||
orientation === "block-vertical-right",
);
}
/**
* Returns the offset of the parent element for converting viewport coordinates to
* parent-relative coordinates. Handles document.body and static positioning.
*/
export function getParentOffsets(parent: HTMLElement | null) {
if (
!parent ||
(parent === document.body && getComputedStyle(parent).position === "static")
) {
return {
parentLeft: -window.pageXOffset,
parentTop: -window.pageYOffset,
};
}
const parentRect = parent.getBoundingClientRect();
const parentScaleX = parentRect.width / parent.offsetWidth;
const parentScaleY = parentRect.height / parent.offsetHeight;
return {
parentLeft: parentRect.left - parent.scrollLeft * parentScaleX,
parentTop: parentRect.top - parent.scrollTop * parentScaleY,
};
}