chore: remove testing code

This commit is contained in:
Palanikannan M
2025-01-03 20:56:22 +05:30
parent 26f344220e
commit 7536a7886a
6 changed files with 0 additions and 1997 deletions

View File

@@ -1,364 +0,0 @@
import { Plugin, EditorState, PluginKey } from "@tiptap/pm/state";
import { EditorView } from "@tiptap/pm/view";
import { dropPoint } from "@tiptap/pm/transform";
import { Editor, Extension } from "@tiptap/core";
import { NodeType, Node as ProseMirrorNode, ResolvedPos } from "@tiptap/pm/model";
interface DropCursorOptions {
/// The color of the cursor. Defaults to `black`. Use `false` to apply no color and rely only on class.
color?: string | false;
/// The precise width of the cursor in pixels. Defaults to 1.
width?: number;
/// A CSS class name to add to the cursor element.
class?: string;
}
/// Create a plugin that, when added to a ProseMirror instance,
/// causes a decoration to show up at the drop position when something
/// is dragged over the editor.
///
/// Nodes may add a `disableDropCursor` property to their spec to
/// control the showing of a drop cursor inside them. This may be a
/// boolean or a function, which will be called with a view and a
/// position, and should return a boolean.
export function dropCursor(options: DropCursorOptions = {}, tiptapEditorOptions: any): Plugin {
const pluginKey = new PluginKey("dropCursor");
return new Plugin({
key: pluginKey,
state: {
init() {
return { dropPosByDropCursorPos: null };
},
apply(tr, state) {
// Get the new state from meta
const meta = tr.getMeta(pluginKey);
if (meta) {
return { dropPosByDropCursorPos: meta.dropPosByDropCursorPos };
}
return state;
},
},
view(editorView) {
return new DropCursorView(editorView, options, tiptapEditorOptions.editor, pluginKey);
},
props: {
handleDrop(view, event, slice, moved) {
const coordinates = { left: event.clientX, top: event.clientY };
const pos = view.posAtCoords(coordinates);
if (!pos) return false;
const $pos = view.state.doc.resolve(pos.pos);
// const { isBetweenNodesOfType: isBetweenLists, position } = isBetweenNodesOfType($pos, "list");
// if (isBetweenLists && position !== null) {
const state = pluginKey.getState(view.state);
let dropPosByDropCursorPos = state?.dropPosByDropCursorPos;
if (dropPosByDropCursorPos != null) {
const tr = view.state.tr;
if (moved) {
// Get the size of content to be deleted
const selection = tr.selection;
const deleteSize = selection.to - selection.from;
// Adjust drop position if it's after the deletion point
if (dropPosByDropCursorPos > selection.from) {
dropPosByDropCursorPos -= deleteSize;
}
tr.deleteSelection();
}
tr.insert(dropPosByDropCursorPos, slice.content);
view.dispatch(tr);
return true;
}
// }
return false;
},
},
});
}
// Add disableDropCursor to NodeSpec
declare module "prosemirror-model" {
interface NodeSpec {
disableDropCursor?:
| boolean
| ((view: EditorView, pos: { pos: number; inside: number }, event: DragEvent) => boolean);
}
}
class DropCursorView {
private width: number;
private color: string | undefined;
private class: string | undefined;
private cursorPos: number | null = null;
private element: HTMLElement | null = null;
private timeout: ReturnType<typeof setTimeout> | null = null;
private handlers: { name: string; handler: (event: Event) => void }[];
private editor: Editor;
constructor(
private readonly editorView: EditorView,
options: DropCursorOptions,
editor: Editor,
private readonly pluginKey: PluginKey
) {
this.width = options.width ?? 1;
this.color = options.color === false ? undefined : options.color || "red";
this.class = options.class;
this.editor = editor;
this.handlers = ["dragover", "dragend", "drop", "dragleave"].map((name) => {
const handler = (e: Event) => {
(this as any)[name](e);
};
editorView.dom.addEventListener(name, handler);
return { name, handler };
});
}
destroy() {
this.handlers.forEach(({ name, handler }) => this.editorView.dom.removeEventListener(name, handler));
}
isBetweenNodesOfType($pos: ResolvedPos, nodeTypeName: string) {
const { doc } = $pos;
const nodeType = doc.type.schema.nodes[nodeTypeName];
function isNodeType(node: ProseMirrorNode | null, type: NodeType) {
return node && node.type === type;
}
let finalPos: number | null = null;
let isBetweenNodesOfType = false;
const listDomNode = this.editorView.nodeDOM($pos.pos);
if (listDomNode) {
const listElement = (listDomNode as HTMLElement)?.closest(".prosemirror-flat-list");
if (listElement) {
isBetweenNodesOfType = true;
}
// __AUTO_GENERATED_PRINT_VAR_START__
console.log("DropCursorView#isBetweenNodesOfType#if listElement: s", listElement); // __AUTO_GENERATED_PRINT_VAR_END__
finalPos = this.editorView.posAtDOM(listElement, 0);
if (listElement.nextElementSibling === null) {
// const
// if (listElement) {
// const nextListElement = listElement.nextElementSibling;
// // __AUTO_GENERATED_PRINT_VAR_START__
// console.log("DropCursorView#isBetweenNodesOfType#if#if nextListElement: s", nextListElement); // __AUTO_GENERATED_PRINT_VAR_END__
// }
// __AUTO_GENERATED_PRINT_VAR_START__
console.log("DropCursorView#isBetweenNodesOfType#if finalPos: %s", finalPos); // __AUTO_GENERATED_PRINT_VAR_END__
}
// let isBetweenNodesOfType = false;
// let positionToShowAndDrop: number | null = null;
//
// const nodeBefore = $pos.nodeBefore;
// const nodeAfter = $pos.nodeAfter;
// const nodeBeforeIsType = isNodeType(nodeBefore, nodeType);
// const nodeAfterIsType = isNodeType(nodeAfter, nodeType);
// if (nodeBeforeIsType || nodeAfterIsType) {
// isBetweenNodesOfType = true;
// positionToShowAndDrop = $pos.pos;
// } else {
// const nextListPos = findNextNodeOfType($pos, nodeType);
// if (nextListPos != null) {
// isBetweenNodesOfType = true;
// positionToShowAndDrop = nextListPos;
// }
// }
// const node = this.editorView.nodeDOM(positionToShowAndDrop);
// const listElement = (node as HTMLElement)?.closest(".prosemirror-flat-list");
// const finalPos = this.editorView.posAtDOM(listElement, 0);
return {
isBetweenNodesOfType,
position: finalPos - 1,
};
}
update(editorView: EditorView, prevState: EditorState) {
if (this.cursorPos != null && prevState.doc != editorView.state.doc) {
if (this.cursorPos > editorView.state.doc.content.size) this.setCursor(null);
else this.updateOverlay();
}
}
setCursor(pos: number | null) {
if (pos == this.cursorPos) return;
this.cursorPos = pos;
if (pos == null) {
this.element!.parentNode!.removeChild(this.element!);
this.element = null;
} else {
this.updateOverlay();
}
}
updateOverlay() {
const $pos = this.editorView.state.doc.resolve(this.cursorPos!);
const isBlock = !$pos.parent.inlineContent;
const isSpecialCase = isNodeAtDepthAndItsParentIsParagraphWhoseParentIsList($pos);
let rect: Partial<DOMRect>;
const editorDOM = this.editorView.dom;
const editorRect = editorDOM.getBoundingClientRect();
const scaleX = editorRect.width / editorDOM.offsetWidth;
const scaleY = editorRect.height / editorDOM.offsetHeight;
if (isBlock) {
const before = $pos.nodeBefore;
const after = $pos.nodeAfter;
if (before || after) {
const node = this.editorView.nodeDOM(this.cursorPos! - (before ? before.nodeSize : 0));
if (node) {
const nodeRect = (node as HTMLElement).getBoundingClientRect();
let top = before ? nodeRect.bottom : nodeRect.top;
if (before && after) {
top = (top + (this.editorView.nodeDOM(this.cursorPos!) as HTMLElement).getBoundingClientRect().top) / 2;
}
const halfWidth = (this.width / 2) * scaleY;
rect = { left: nodeRect.left, right: nodeRect.right, top: top - halfWidth, bottom: top + halfWidth };
}
}
}
if (!rect) {
const coords = this.editorView.coordsAtPos(this.cursorPos!);
const halfWidth = (this.width / 2) * scaleX;
rect = { left: coords.left - halfWidth, right: coords.left + halfWidth, top: coords.top, bottom: coords.bottom };
}
const parent = this.editorView.dom.offsetParent as HTMLElement;
if (!this.element) {
this.element = parent.appendChild(document.createElement("div"));
if (this.class) this.element.className = this.class;
this.element.style.cssText = "position: absolute; z-index: 50; pointer-events: none;";
if (this.color) {
this.element.style.backgroundColor = this.color;
}
}
this.element.classList.toggle("prosemirror-dropcursor-block", isBlock);
this.element.classList.toggle("prosemirror-dropcursor-inline", !isBlock);
let parentLeft: number, parentTop: number;
if (!parent || (parent == document.body && getComputedStyle(parent).position == "static")) {
parentLeft = -window.scrollX;
parentTop = -window.scrollY;
} else {
const rect = parent.getBoundingClientRect();
const parentScaleX = rect.width / parent.offsetWidth,
parentScaleY = rect.height / parent.offsetHeight;
parentLeft = rect.left - parent.scrollLeft * parentScaleX;
parentTop = rect.top - parent.scrollTop * parentScaleY;
}
this.element.style.left = (rect.left - parentLeft) / scaleX + "px";
this.element.style.top = (rect.top - parentTop) / scaleY + "px";
this.element.style.width = (rect.right - rect.left) / scaleX + "px";
this.element.style.height = (rect.bottom - rect.top) / scaleY + "px";
}
scheduleRemoval(timeout: number) {
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.setCursor(null), timeout);
}
dragover(event: DragEvent) {
if (!this.editorView.editable) return;
const pos = this.editorView.posAtCoords({ left: event.clientX, top: event.clientY });
if (pos) {
const $pos = this.editorView.state.doc.resolve(pos.pos);
const node = pos.inside >= 0 && this.editorView.state.doc.nodeAt(pos.inside);
const disableDropCursor = node && node.type.spec.disableDropCursor;
const disabled =
typeof disableDropCursor == "function" ? disableDropCursor(this.editorView, pos, event) : disableDropCursor;
if (!disabled) {
const { isBetweenNodesOfType: isBetweenNodesOfTypeLists, position } = this.isBetweenNodesOfType($pos, "list");
if (isBetweenNodesOfTypeLists && position !== undefined) {
this.dropPosByDropCursorPos = position;
this.setCursor(position);
return;
}
let target = pos.pos;
if (this.editorView.dragging && this.editorView.dragging.slice) {
const point = dropPoint(this.editorView.state.doc, target, this.editorView.dragging.slice);
if (point != null) target = point;
}
this.setCursor(target);
this.scheduleRemoval(5000);
}
}
}
dragend() {
this.scheduleRemoval(20);
}
drop() {
this.scheduleRemoval(20);
}
dragleave(event: DragEvent) {
const relatedTarget = event.relatedTarget as Node | null;
if (relatedTarget && !this.editorView.dom.contains(relatedTarget)) {
this.setCursor(null);
}
}
set dropPosByDropCursorPos(pos: number | null) {
const tr = this.editorView.state.tr;
tr.setMeta(this.pluginKey, { dropPosByDropCursorPos: pos });
this.editorView.dispatch(tr);
}
get dropPosByDropCursorPos(): number | null {
return this.pluginKey.getState(this.editorView.state)?.dropPosByDropCursorPos;
}
}
export const DropCursorExtension = Extension.create({
name: "dropCursor",
addProseMirrorPlugins(this) {
return [
dropCursor(
{
width: 2,
class: "transition-all duration-200 ease-[cubic-bezier(0.165, 0.84, 0.44, 1)] text-custom-text-300",
},
this
),
];
},
});
function findNextNodeOfType($pos: ResolvedPos, nodeType: NodeType): number | null {
for (let i = $pos.pos; i < $pos.doc.content.size; i++) {
const node = $pos.doc.nodeAt(i);
if (node && node.type === nodeType) {
return i;
}
}
return null;
}
function isNodeAtDepthAndItsParentIsParagraphWhoseParentIsList($pos: ResolvedPos): boolean {
const depth = $pos.depth;
if (depth >= 0) {
const parent = $pos.node(depth);
const grandParent = $pos.node(depth - 1);
return parent.type.name === "paragraph" && grandParent.type.name === "list";
}
return false;
}

View File

@@ -1,325 +0,0 @@
import { Plugin, EditorState, NodeSelection } from "@tiptap/pm/state";
import { EditorView } from "@tiptap/pm/view";
import { dropPoint } from "@tiptap/pm/transform";
import { Editor, Extension } from "@tiptap/core";
import { Node as ProseMirrorNode, ResolvedPos } from "@tiptap/pm/model";
interface DropCursorOptions {
/// The color of the cursor. Defaults to `black`. Use `false` to apply no color and rely only on class.
color?: string | false;
/// The precise width of the cursor in pixels. Defaults to 1.
width?: number;
/// A CSS class name to add to the cursor element.
class?: string;
}
/// Create a plugin that, when added to a ProseMirror instance,
/// causes a decoration to show up at the drop position when something
/// is dragged over the editor.
///
/// Nodes may add a `disableDropCursor` property to their spec to
/// control the showing of a drop cursor inside them. This may be a
/// boolean or a function, which will be called with a view and a
/// position, and should return a boolean.
export function dropCursor(
options: DropCursorOptions = {},
tiptapEditorOptions: {
name: string;
options: any;
storage: any;
editor: Editor;
parent: () => Plugin<any>[];
}
): Plugin {
return new Plugin({
view(editorView) {
return new DropCursorView(editorView, options, tiptapEditorOptions.editor);
},
props: {
handleDrop(view, event) {
const pos = view.posAtCoords({ left: event.clientX, top: event.clientY });
if (pos) {
const $pos = view.state.doc.resolve(pos.pos);
// Only prevent default if we're between lists
if (isBetweenNodesOfType($pos, "list").isBetweenNodesOfType) {
return true;
}
}
return false; // Let other drop handlers work normally
},
},
});
}
function isBetweenNodesOfType(
$pos: ResolvedPos,
nodeType: string
): { isBetweenNodesOfType: boolean; isDirectlyBetweenLists: boolean; position?: number } {
// Check direct siblings first
if ($pos.nodeBefore?.type.name === nodeType && $pos.nodeAfter?.type.name === nodeType) {
return { isBetweenNodesOfType: true, isDirectlyBetweenLists: true, position: $pos.pos };
}
// Helper function to get all parent types up to root and their positions
const getParentTypes = (
node: ProseMirrorNode | null,
$pos: ResolvedPos
): { types: Set<string>; position?: number } => {
const types = new Set<string>();
if (!node) return { types };
types.add(node.type.name);
if (node.type.name === nodeType) {
return { types, position: $pos.pos };
}
// Traverse up through all depths
for (let depth = $pos.depth; depth > 0; depth--) {
const parent = $pos.node(depth);
types.add(parent.type.name);
if (parent.type.name === nodeType) {
return { types, position: $pos.before(depth) };
}
}
return { types };
};
// Get parent types and positions for both before and after nodes
const before = getParentTypes($pos.nodeBefore, $pos);
const after = getParentTypes($pos.nodeAfter, $pos);
console.log("before", before.position);
console.log("after", after.position);
// Check if both branches contain the nodeType and return the relevant position
if (before.types.has(nodeType) && before.position !== undefined) {
return { isBetweenNodesOfType: true, isDirectlyBetweenLists: false, position: before.position };
}
if (after.types.has(nodeType) && after.position !== undefined) {
return { isBetweenNodesOfType: true, isDirectlyBetweenLists: false, position: after.position };
}
return { isBetweenNodesOfType: false, isDirectlyBetweenLists: false };
}
// Add disableDropCursor to NodeSpec
declare module "prosemirror-model" {
interface NodeSpec {
disableDropCursor?:
| boolean
| ((view: EditorView, pos: { pos: number; inside: number }, event: DragEvent) => boolean);
}
}
class DropCursorView {
private width: number;
private color: string | undefined;
private class: string | undefined;
private cursorPos: number | null = null;
private element: HTMLElement | null = null;
private timeout: ReturnType<typeof setTimeout> | null = null;
private handlers: { name: string; handler: (event: Event) => void }[];
private editor: Editor;
private lastValidPosition: number | null = null;
constructor(
private readonly editorView: EditorView,
options: DropCursorOptions,
editor: Editor
) {
this.width = options.width ?? 1;
this.color = options.color === false ? undefined : options.color || "red";
this.class = options.class;
this.editor = editor;
this.handlers = ["dragover", "dragend", "drop", "dragleave"].map((name) => {
const handler = (e: Event) => {
(this as any)[name](e);
};
editorView.dom.addEventListener(name, handler);
return { name, handler };
});
}
destroy() {
this.handlers.forEach(({ name, handler }) => this.editorView.dom.removeEventListener(name, handler));
}
update(editorView: EditorView, prevState: EditorState) {
if (this.cursorPos != null && prevState.doc != editorView.state.doc) {
if (this.cursorPos > editorView.state.doc.content.size) this.setCursor(null);
else this.updateOverlay();
}
}
setCursor(pos: number | null) {
if (pos == this.cursorPos) return;
this.cursorPos = pos;
if (pos == null) {
this.element!.parentNode!.removeChild(this.element!);
this.element = null;
} else {
this.updateOverlay();
}
}
updateOverlay() {
const $pos = this.editorView.state.doc.resolve(this.cursorPos!);
const isBlock = !$pos.parent.inlineContent;
let rect: Partial<DOMRect>;
const editorDOM = this.editorView.dom;
const editorRect = editorDOM.getBoundingClientRect();
const scaleX = editorRect.width / editorDOM.offsetWidth;
const scaleY = editorRect.height / editorDOM.offsetHeight;
if (isBlock) {
const before = $pos.nodeBefore;
const after = $pos.nodeAfter;
if (before || after) {
const node = this.editorView.nodeDOM(this.cursorPos! - (before ? before.nodeSize : 0));
if (node) {
const nodeRect = (node as HTMLElement).getBoundingClientRect();
let top = before ? nodeRect.bottom : nodeRect.top;
if (before && after) {
top = (top + (this.editorView.nodeDOM(this.cursorPos!) as HTMLElement).getBoundingClientRect().top) / 2;
}
const halfWidth = (this.width / 2) * scaleY;
rect = { left: nodeRect.left, right: nodeRect.right, top: top - halfWidth, bottom: top + halfWidth };
}
}
}
if (!rect) {
const coords = this.editorView.coordsAtPos(this.cursorPos!);
const halfWidth = (this.width / 2) * scaleX;
rect = { left: coords.left - halfWidth, right: coords.left + halfWidth, top: coords.top, bottom: coords.bottom };
}
const parent = this.editorView.dom.offsetParent as HTMLElement;
if (!this.element) {
this.element = parent.appendChild(document.createElement("div"));
if (this.class) this.element.className = this.class;
this.element.style.cssText = "position: absolute; z-index: 50; pointer-events: none;";
if (this.color) {
this.element.style.backgroundColor = this.color;
}
}
this.element.classList.toggle("prosemirror-dropcursor-block", isBlock);
this.element.classList.toggle("prosemirror-dropcursor-inline", !isBlock);
let parentLeft: number, parentTop: number;
if (!parent || (parent == document.body && getComputedStyle(parent).position == "static")) {
parentLeft = -window.scrollX;
parentTop = -window.scrollY;
} else {
const rect = parent.getBoundingClientRect();
const parentScaleX = rect.width / parent.offsetWidth,
parentScaleY = rect.height / parent.offsetHeight;
parentLeft = rect.left - parent.scrollLeft * parentScaleX;
parentTop = rect.top - parent.scrollTop * parentScaleY;
}
this.element.style.left = (rect.left - parentLeft) / scaleX + "px";
this.element.style.top = (rect.top - parentTop) / scaleY + "px";
this.element.style.width = (rect.right - rect.left) / scaleX + "px";
this.element.style.height = (rect.bottom - rect.top) / scaleY + "px";
}
scheduleRemoval(timeout: number) {
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.setCursor(null), timeout);
}
dragover(event: DragEvent) {
if (!this.editorView.editable) return;
const pos = this.editorView.posAtCoords({ left: event.clientX, top: event.clientY });
if (pos) {
const $pos = this.editorView.state.doc.resolve(pos.pos);
const node = pos.inside >= 0 && this.editorView.state.doc.nodeAt(pos.inside);
const disableDropCursor = node && node.type.spec.disableDropCursor;
const disabled =
typeof disableDropCursor == "function" ? disableDropCursor(this.editorView, pos, event) : disableDropCursor;
if (!disabled) {
const { isBetweenNodesOfType: isBetweenNodesOfTypeLists, position } = isBetweenNodesOfType($pos, "list");
if (isBetweenNodesOfTypeLists && position !== undefined) {
this.lastValidPosition = position;
this.setCursor(position);
console.log("cursor set at ", position);
return;
}
let target = pos.pos;
if (this.editorView.dragging && this.editorView.dragging.slice) {
const point = dropPoint(this.editorView.state.doc, target, this.editorView.dragging.slice);
if (point != null) target = point;
}
this.setCursor(target);
this.scheduleRemoval(5000);
}
}
}
dragend() {
this.scheduleRemoval(20);
}
drop(event: DragEvent) {
const pos = this.editorView.posAtCoords({ left: event.clientX, top: event.clientY });
if (pos) {
const $pos = this.editorView.state.doc.resolve(pos.pos);
if (isBetweenNodesOfType($pos, "list").isBetweenNodesOfType) {
event.preventDefault();
event.stopPropagation();
let draggedContent: ProseMirrorNode | null = null;
let originalFrom: number | null = null;
let originalTo: number | null = null;
if (this.lastValidPosition !== null) {
if (this.editorView.state.selection instanceof NodeSelection) {
draggedContent = this.editorView.state.selection.node;
originalFrom = this.editorView.state.selection.from;
originalTo = this.editorView.state.selection.to;
}
if (draggedContent && originalFrom !== null && originalTo !== null) {
const tr = this.editorView.state.tr;
// Remove the node from its original position
tr.delete(originalFrom, originalTo);
// Insert the node at the new position
tr.insert(this.lastValidPosition, draggedContent);
// Apply the transaction
this.editorView.dispatch(tr);
}
this.lastValidPosition = null;
}
}
}
this.scheduleRemoval(20);
return true;
}
dragleave(event: DragEvent) {
const relatedTarget = event.relatedTarget as Node | null;
if (relatedTarget && !this.editorView.dom.contains(relatedTarget)) {
this.setCursor(null);
}
}
}
export const DropCursorExtension = Extension.create({
name: "dropCursor",
addProseMirrorPlugins(this) {
return [
dropCursor(
{
width: 2,
class: "transition-all duration-200 ease-[cubic-bezier(0.165, 0.84, 0.44, 1)] text-custom-text-300",
},
this
),
];
},
});

View File

@@ -1,374 +0,0 @@
import { Plugin, EditorState, NodeSelection, PluginKey } from "@tiptap/pm/state";
import { EditorView } from "@tiptap/pm/view";
import { dropPoint } from "@tiptap/pm/transform";
import { Editor, Extension } from "@tiptap/core";
import { NodeType, Node as ProseMirrorNode, ResolvedPos } from "@tiptap/pm/model";
interface DropCursorOptions {
/// The color of the cursor. Defaults to `black`. Use `false` to apply no color and rely only on class.
color?: string | false;
/// The precise width of the cursor in pixels. Defaults to 1.
width?: number;
/// A CSS class name to add to the cursor element.
class?: string;
}
/// Create a plugin that, when added to a ProseMirror instance,
/// causes a decoration to show up at the drop position when something
/// is dragged over the editor.
///
/// Nodes may add a `disableDropCursor` property to their spec to
/// control the showing of a drop cursor inside them. This may be a
/// boolean or a function, which will be called with a view and a
/// position, and should return a boolean.
export function dropCursor(options: DropCursorOptions = {}, tiptapEditorOptions: any): Plugin {
const pluginKey = new PluginKey("dropCursor");
return new Plugin({
key: pluginKey,
state: {
init() {
return { dropPosByDropCursorPos: null };
},
apply(tr, state) {
// Get the new state from meta
const meta = tr.getMeta(pluginKey);
if (meta) {
return { dropPosByDropCursorPos: meta.dropPosByDropCursorPos };
}
return state;
},
},
view(editorView) {
return new DropCursorView(editorView, options, tiptapEditorOptions.editor, pluginKey);
},
props: {
handleDrop(view, event, slice, moved) {
console.log("aaya");
const coordinates = { left: event.clientX, top: event.clientY };
const pos = view.posAtCoords(coordinates);
// if (!pos) return false;
const $pos = view.state.doc.resolve(pos.pos);
const { isBetweenNodesOfType: isBetweenLists } = isBetweenNodesOfType($pos, "list");
if (isBetweenLists) {
console.log("asdff");
const state = pluginKey.getState(view.state);
const dropPosByDropCursorPos = state?.dropPosByDropCursorPos;
// __AUTO_GENERATED_PRINT_VAR_START__
console.log("dropCursor#handleDrop#if dropPosByDropCursorPos: %s", dropPosByDropCursorPos + 1); // __AUTO_GENERATED_PRINT_VAR_END__
if (dropPosByDropCursorPos != null) {
const tr = view.state.tr;
if (moved) {
tr.deleteSelection();
}
tr.insert(dropPosByDropCursorPos + 1, slice.content);
view.dispatch(tr);
}
return true;
}
return false;
},
},
});
}
// Add disableDropCursor to NodeSpec
declare module "prosemirror-model" {
interface NodeSpec {
disableDropCursor?:
| boolean
| ((view: EditorView, pos: { pos: number; inside: number }, event: DragEvent) => boolean);
}
}
class DropCursorView {
private width: number;
private color: string | undefined;
private class: string | undefined;
private cursorPos: number | null = null;
private element: HTMLElement | null = null;
private timeout: ReturnType<typeof setTimeout> | null = null;
private handlers: { name: string; handler: (event: Event) => void }[];
private editor: Editor;
constructor(
private readonly editorView: EditorView,
options: DropCursorOptions,
editor: Editor,
private readonly pluginKey: PluginKey
) {
this.width = options.width ?? 1;
this.color = options.color === false ? undefined : options.color || "red";
this.class = options.class;
this.editor = editor;
this.handlers = ["dragover", "dragend", "drop", "dragleave"].map((name) => {
const handler = (e: Event) => {
(this as any)[name](e);
};
editorView.dom.addEventListener(name, handler);
return { name, handler };
});
}
destroy() {
this.handlers.forEach(({ name, handler }) => this.editorView.dom.removeEventListener(name, handler));
}
update(editorView: EditorView, prevState: EditorState) {
if (this.cursorPos != null && prevState.doc != editorView.state.doc) {
if (this.cursorPos > editorView.state.doc.content.size) this.setCursor(null);
else this.updateOverlay();
}
}
setCursor(pos: number | null) {
if (pos == this.cursorPos) return;
this.cursorPos = pos;
if (pos == null) {
this.element!.parentNode!.removeChild(this.element!);
this.element = null;
} else {
this.updateOverlay();
}
}
updateOverlay() {
const $pos = this.editorView.state.doc.resolve(this.cursorPos!);
const isBlock = !$pos.parent.inlineContent;
let rect: Partial<DOMRect>;
const editorDOM = this.editorView.dom;
const editorRect = editorDOM.getBoundingClientRect();
const scaleX = editorRect.width / editorDOM.offsetWidth;
const scaleY = editorRect.height / editorDOM.offsetHeight;
if (isBlock) {
const before = $pos.nodeBefore;
const after = $pos.nodeAfter;
if (before || after) {
const node = this.editorView.nodeDOM(this.cursorPos! - (before ? before.nodeSize : 0));
if (node) {
const nodeRect = (node as HTMLElement).getBoundingClientRect();
let top = before ? nodeRect.bottom : nodeRect.top;
if (before && after) {
top = (top + (this.editorView.nodeDOM(this.cursorPos!) as HTMLElement).getBoundingClientRect().top) / 2;
}
const halfWidth = (this.width / 2) * scaleY;
rect = { left: nodeRect.left, right: nodeRect.right, top: top - halfWidth, bottom: top + halfWidth };
}
}
}
if (!rect) {
const coords = this.editorView.coordsAtPos(this.cursorPos!);
const halfWidth = (this.width / 2) * scaleX;
rect = { left: coords.left - halfWidth, right: coords.left + halfWidth, top: coords.top, bottom: coords.bottom };
}
const parent = this.editorView.dom.offsetParent as HTMLElement;
if (!this.element) {
this.element = parent.appendChild(document.createElement("div"));
if (this.class) this.element.className = this.class;
this.element.style.cssText = "position: absolute; z-index: 50; pointer-events: none;";
if (this.color) {
this.element.style.backgroundColor = this.color;
}
}
this.element.classList.toggle("prosemirror-dropcursor-block", isBlock);
this.element.classList.toggle("prosemirror-dropcursor-inline", !isBlock);
let parentLeft: number, parentTop: number;
if (!parent || (parent == document.body && getComputedStyle(parent).position == "static")) {
parentLeft = -window.scrollX;
parentTop = -window.scrollY;
} else {
const rect = parent.getBoundingClientRect();
const parentScaleX = rect.width / parent.offsetWidth,
parentScaleY = rect.height / parent.offsetHeight;
parentLeft = rect.left - parent.scrollLeft * parentScaleX;
parentTop = rect.top - parent.scrollTop * parentScaleY;
}
this.element.style.left = (rect.left - parentLeft) / scaleX + "px";
this.element.style.top = (rect.top - parentTop) / scaleY + "px";
this.element.style.width = (rect.right - rect.left) / scaleX + "px";
this.element.style.height = (rect.bottom - rect.top) / scaleY + "px";
}
scheduleRemoval(timeout: number) {
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.setCursor(null), timeout);
}
dragover(event: DragEvent) {
if (!this.editorView.editable) return;
const pos = this.editorView.posAtCoords({ left: event.clientX, top: event.clientY });
if (pos) {
const $pos = this.editorView.state.doc.resolve(pos.pos);
const node = pos.inside >= 0 && this.editorView.state.doc.nodeAt(pos.inside);
const disableDropCursor = node && node.type.spec.disableDropCursor;
const disabled =
typeof disableDropCursor == "function" ? disableDropCursor(this.editorView, pos, event) : disableDropCursor;
if (!disabled) {
const { isBetweenNodesOfType: isBetweenNodesOfTypeLists, position } = isBetweenNodesOfType($pos, "list");
if (isBetweenNodesOfTypeLists && position !== undefined) {
this.dropPosByDropCursorPos = position;
this.setCursor(position);
return;
}
let target = pos.pos;
if (this.editorView.dragging && this.editorView.dragging.slice) {
const point = dropPoint(this.editorView.state.doc, target, this.editorView.dragging.slice);
if (point != null) target = point;
}
this.setCursor(target);
this.scheduleRemoval(5000);
}
}
}
dragend() {
this.scheduleRemoval(20);
}
drop() {
this.scheduleRemoval(20);
}
dragleave(event: DragEvent) {
const relatedTarget = event.relatedTarget as Node | null;
if (relatedTarget && !this.editorView.dom.contains(relatedTarget)) {
this.setCursor(null);
}
}
set dropPosByDropCursorPos(pos: number | null) {
const tr = this.editorView.state.tr;
tr.setMeta(this.pluginKey, { dropPosByDropCursorPos: pos });
this.editorView.dispatch(tr);
}
get dropPosByDropCursorPos(): number | null {
return this.pluginKey.getState(this.editorView.state)?.dropPosByDropCursorPos;
}
}
export const DropCursorExtension = Extension.create({
name: "dropCursor",
addProseMirrorPlugins(this) {
return [
dropCursor(
{
width: 2,
class: "transition-all duration-200 ease-[cubic-bezier(0.165, 0.84, 0.44, 1)] text-custom-text-300",
},
this
),
];
},
});
function isBetweenNodesOfType($pos: ResolvedPos, nodeTypeName: string) {
const { doc } = $pos;
const nodeType = doc.type.schema.nodes[nodeTypeName];
const listItemType = doc.type.schema.nodes["list_item"]; // Replace with your list item node type name
function isNodeType(node: ProseMirrorNode | null, type: NodeType) {
return node && node.type === type;
}
let isBetweenNodesOfType = false;
let isDirectlyBetweenLists = false;
let position: number | null = null;
// Check if we are inside a list item
let foundListItem = false;
for (let depth = $pos.depth; depth >= 0; depth--) {
const node = $pos.node(depth);
if (isNodeType(node, listItemType)) {
foundListItem = true;
const listItemPos = $pos.before(depth);
const parent = $pos.node(depth - 1); // This should be the list node
if (parent && isNodeType(parent, nodeType)) {
const index = findChildIndex(parent, $pos.before(depth + 1));
const nextIndex = index + 1;
if (nextIndex < parent.childCount) {
// There is a next sibling list item
position = listItemPos + node.nodeSize;
} else {
// No siblings, insert at the end of the list
position = $pos.end(depth - 1);
}
}
break;
}
}
if (foundListItem) {
isBetweenNodesOfType = true;
isDirectlyBetweenLists = false;
}
// If not found inside a list item, check if we are directly between list nodes
const nodeBefore = $pos.nodeBefore;
const nodeAfter = $pos.nodeAfter;
const nodeBeforeIsType = isNodeType(nodeBefore, nodeType);
const nodeAfterIsType = isNodeType(nodeAfter, nodeType);
if (nodeBeforeIsType && nodeAfterIsType) {
// Cursor is directly between two list nodes
isBetweenNodesOfType = true;
isDirectlyBetweenLists = true;
position = $pos.pos;
} else if (nodeBeforeIsType || nodeAfterIsType) {
isBetweenNodesOfType = true;
isDirectlyBetweenLists = false;
position = $pos.pos;
} else if (!foundListItem) {
// If not between lists or inside a list item, look ahead for the next list
const nextListPos = findNextNodeOfType($pos, nodeType);
if (nextListPos != null) {
isBetweenNodesOfType = true;
isDirectlyBetweenLists = false;
position = nextListPos;
}
}
return {
isBetweenNodesOfType,
isDirectlyBetweenLists,
position,
};
}
function findChildIndex(parent: ProseMirrorNode, pos: number): number {
let offset = 0;
for (let i = 0; i < parent.childCount; i++) {
const child = parent.child(i);
if (offset + child.nodeSize > pos) {
return i;
}
offset += child.nodeSize;
}
return -1; // Return -1 if not found
}
function findNextNodeOfType($pos: ResolvedPos, nodeType: NodeType): number | null {
for (let i = $pos.pos; i < $pos.doc.content.size; i++) {
const node = $pos.doc.nodeAt(i);
if (node && node.type === nodeType) {
return i;
}
}
return null;
}

View File

@@ -1,359 +0,0 @@
import { Plugin, EditorState, NodeSelection, PluginKey } from "@tiptap/pm/state";
import { EditorView } from "@tiptap/pm/view";
import { dropPoint } from "@tiptap/pm/transform";
import { Editor, Extension } from "@tiptap/core";
import { NodeType, Node as ProseMirrorNode, ResolvedPos } from "@tiptap/pm/model";
interface DropCursorOptions {
/// The color of the cursor. Defaults to `black`. Use `false` to apply no color and rely only on class.
color?: string | false;
/// The precise width of the cursor in pixels. Defaults to 1.
width?: number;
/// A CSS class name to add to the cursor element.
class?: string;
}
/// Create a plugin that, when added to a ProseMirror instance,
/// causes a decoration to show up at the drop position when something
/// is dragged over the editor.
///
/// Nodes may add a `disableDropCursor` property to their spec to
/// control the showing of a drop cursor inside them. This may be a
/// boolean or a function, which will be called with a view and a
/// position, and should return a boolean.
export function dropCursor(options: DropCursorOptions = {}, tiptapEditorOptions: any): Plugin {
const pluginKey = new PluginKey("dropCursor");
return new Plugin({
key: pluginKey,
state: {
init() {
return { dropPosByDropCursorPos: null };
},
apply(tr, state) {
// Get the new state from meta
const meta = tr.getMeta(pluginKey);
if (meta) {
return { dropPosByDropCursorPos: meta.dropPosByDropCursorPos };
}
return state;
},
},
view(editorView) {
return new DropCursorView(editorView, options, tiptapEditorOptions.editor, pluginKey);
},
props: {
handleDrop(view, event, slice, moved) {
const coordinates = { left: event.clientX, top: event.clientY };
const pos = view.posAtCoords(coordinates);
if (!pos) return false;
const $pos = view.state.doc.resolve(pos.pos);
const { isBetweenNodesOfType: isBetweenLists, position } = isBetweenNodesOfType($pos, "list");
if (isBetweenLists && position !== null) {
const state = pluginKey.getState(view.state);
let dropPosByDropCursorPos = state?.dropPosByDropCursorPos;
if (dropPosByDropCursorPos != null) {
const tr = view.state.tr;
if (moved) {
// Get the size of content to be deleted
const selection = tr.selection;
const deleteSize = selection.to - selection.from;
// Adjust drop position if it's after the deletion point
if (dropPosByDropCursorPos > selection.from) {
dropPosByDropCursorPos -= deleteSize;
}
tr.deleteSelection();
}
tr.insert(dropPosByDropCursorPos, slice.content);
view.dispatch(tr);
return true;
}
}
return false;
},
},
});
}
// Add disableDropCursor to NodeSpec
declare module "prosemirror-model" {
interface NodeSpec {
disableDropCursor?:
| boolean
| ((view: EditorView, pos: { pos: number; inside: number }, event: DragEvent) => boolean);
}
}
class DropCursorView {
private width: number;
private color: string | undefined;
private class: string | undefined;
private cursorPos: number | null = null;
private element: HTMLElement | null = null;
private timeout: ReturnType<typeof setTimeout> | null = null;
private handlers: { name: string; handler: (event: Event) => void }[];
private editor: Editor;
constructor(
private readonly editorView: EditorView,
options: DropCursorOptions,
editor: Editor,
private readonly pluginKey: PluginKey
) {
this.width = options.width ?? 1;
this.color = options.color === false ? undefined : options.color || "red";
this.class = options.class;
this.editor = editor;
this.handlers = ["dragover", "dragend", "drop", "dragleave"].map((name) => {
const handler = (e: Event) => {
(this as any)[name](e);
};
editorView.dom.addEventListener(name, handler);
return { name, handler };
});
}
destroy() {
this.handlers.forEach(({ name, handler }) => this.editorView.dom.removeEventListener(name, handler));
}
update(editorView: EditorView, prevState: EditorState) {
if (this.cursorPos != null && prevState.doc != editorView.state.doc) {
if (this.cursorPos > editorView.state.doc.content.size) this.setCursor(null);
else this.updateOverlay();
}
}
setCursor(pos: number | null) {
if (pos == this.cursorPos) return;
this.cursorPos = pos;
if (pos == null) {
this.element!.parentNode!.removeChild(this.element!);
this.element = null;
} else {
this.updateOverlay();
}
}
updateOverlay() {
const $pos = this.editorView.state.doc.resolve(this.cursorPos!);
const isBlock = !$pos.parent.inlineContent;
const isSpecialCase = isNodeAtDepthAndItsParentIsParagraphWhoseParentIsList($pos);
let rect: Partial<DOMRect>;
const editorDOM = this.editorView.dom;
const editorRect = editorDOM.getBoundingClientRect();
const scaleX = editorRect.width / editorDOM.offsetWidth;
const scaleY = editorRect.height / editorDOM.offsetHeight;
if (isBlock) {
const before = $pos.nodeBefore;
const after = $pos.nodeAfter;
if (before || after) {
const node = this.editorView.nodeDOM(this.cursorPos! - (before ? before.nodeSize : 0));
if (node) {
const nodeRect = (node as HTMLElement).getBoundingClientRect();
let top = before ? nodeRect.bottom : nodeRect.top;
if (before && after) {
top = (top + (this.editorView.nodeDOM(this.cursorPos!) as HTMLElement).getBoundingClientRect().top) / 2;
}
const halfWidth = (this.width / 2) * scaleY;
rect = { left: nodeRect.left, right: nodeRect.right, top: top - halfWidth, bottom: top + halfWidth };
}
}
}
if (!rect) {
const coords = this.editorView.coordsAtPos(this.cursorPos!);
const halfWidth = (this.width / 2) * scaleX;
rect = { left: coords.left - halfWidth, right: coords.left + halfWidth, top: coords.top, bottom: coords.bottom };
}
const parent = this.editorView.dom.offsetParent as HTMLElement;
if (!this.element) {
this.element = parent.appendChild(document.createElement("div"));
if (this.class) this.element.className = this.class;
this.element.style.cssText = "position: absolute; z-index: 50; pointer-events: none;";
if (this.color) {
this.element.style.backgroundColor = this.color;
}
}
this.element.classList.toggle("prosemirror-dropcursor-block", isBlock);
this.element.classList.toggle("prosemirror-dropcursor-inline", !isBlock);
let parentLeft: number, parentTop: number;
if (!parent || (parent == document.body && getComputedStyle(parent).position == "static")) {
parentLeft = -window.scrollX;
parentTop = -window.scrollY;
} else {
const rect = parent.getBoundingClientRect();
const parentScaleX = rect.width / parent.offsetWidth,
parentScaleY = rect.height / parent.offsetHeight;
parentLeft = rect.left - parent.scrollLeft * parentScaleX;
parentTop = rect.top - parent.scrollTop * parentScaleY;
}
this.element.style.left = (rect.left - parentLeft) / scaleX + "px";
this.element.style.top = (rect.top - parentTop) / scaleY + "px";
this.element.style.width = (rect.right - rect.left) / scaleX + "px";
this.element.style.height = (rect.bottom - rect.top) / scaleY + "px";
}
scheduleRemoval(timeout: number) {
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.setCursor(null), timeout);
}
dragover(event: DragEvent) {
if (!this.editorView.editable) return;
const pos = this.editorView.posAtCoords({ left: event.clientX, top: event.clientY });
if (pos) {
const $pos = this.editorView.state.doc.resolve(pos.pos);
const node = pos.inside >= 0 && this.editorView.state.doc.nodeAt(pos.inside);
const disableDropCursor = node && node.type.spec.disableDropCursor;
const disabled =
typeof disableDropCursor == "function" ? disableDropCursor(this.editorView, pos, event) : disableDropCursor;
let finalPos: number | null = null;
if (!disabled) {
const {
isBetweenNodesOfType: isBetweenNodesOfTypeLists,
position,
isSpecialCase,
} = isBetweenNodesOfType($pos, "list");
const node = this.editorView.nodeDOM(position);
const listElement = (node as HTMLElement).closest(".prosemirror-flat-list");
finalPos = this.editorView.posAtDOM(listElement, 0);
const $pos1 = this.editorView.state.doc.resolve(finalPos);
console.log("asfd", $pos1);
if (isBetweenNodesOfTypeLists && position !== undefined) {
this.dropPosByDropCursorPos = finalPos - 1;
this.setCursor(finalPos - 1);
return;
}
let target = pos.pos;
if (this.editorView.dragging && this.editorView.dragging.slice) {
const point = dropPoint(this.editorView.state.doc, target, this.editorView.dragging.slice);
if (point != null) target = point;
}
this.setCursor(target);
this.scheduleRemoval(5000);
}
}
}
dragend() {
this.scheduleRemoval(20);
}
drop() {
this.scheduleRemoval(20);
}
dragleave(event: DragEvent) {
const relatedTarget = event.relatedTarget as Node | null;
if (relatedTarget && !this.editorView.dom.contains(relatedTarget)) {
this.setCursor(null);
}
}
set dropPosByDropCursorPos(pos: number | null) {
const tr = this.editorView.state.tr;
tr.setMeta(this.pluginKey, { dropPosByDropCursorPos: pos });
this.editorView.dispatch(tr);
}
get dropPosByDropCursorPos(): number | null {
return this.pluginKey.getState(this.editorView.state)?.dropPosByDropCursorPos;
}
}
export const DropCursorExtension = Extension.create({
name: "dropCursor",
addProseMirrorPlugins(this) {
return [
dropCursor(
{
width: 2,
class: "transition-all duration-200 ease-[cubic-bezier(0.165, 0.84, 0.44, 1)] text-custom-text-300",
},
this
),
];
},
});
function isBetweenNodesOfType($pos: ResolvedPos, nodeTypeName: string) {
const { doc } = $pos;
const nodeType = doc.type.schema.nodes[nodeTypeName];
function isNodeType(node: ProseMirrorNode | null, type: NodeType) {
return node && node.type === type;
}
let isBetweenNodesOfType = false;
let isDirectlyBetweenNodes = false;
let positionToShowAndDrop: number | null = null;
const isSpecialCase = isNodeAtDepthAndItsParentIsParagraphWhoseParentIsList($pos);
const nodeBefore = $pos.nodeBefore;
const nodeAfter = $pos.nodeAfter;
const nodeBeforeIsType = isNodeType(nodeBefore, nodeType);
const nodeAfterIsType = isNodeType(nodeAfter, nodeType);
if (nodeBeforeIsType && nodeAfterIsType) {
isBetweenNodesOfType = true;
isDirectlyBetweenNodes = true;
positionToShowAndDrop = $pos.pos;
} else if (nodeBeforeIsType || nodeAfterIsType) {
isBetweenNodesOfType = true;
isDirectlyBetweenNodes = false;
positionToShowAndDrop = $pos.pos;
} else {
const nextListPos = findNextNodeOfType($pos, nodeType);
if (nextListPos != null) {
isBetweenNodesOfType = true;
isDirectlyBetweenNodes = false;
positionToShowAndDrop = nextListPos;
}
}
return {
isBetweenNodesOfType,
isDirectlyBetweenLists: isDirectlyBetweenNodes,
position: positionToShowAndDrop,
isSpecialCase,
};
}
function findNextNodeOfType($pos: ResolvedPos, nodeType: NodeType): number | null {
for (let i = $pos.pos; i < $pos.doc.content.size; i++) {
const node = $pos.doc.nodeAt(i);
if (node && node.type === nodeType) {
return i;
}
}
return null;
}
function isNodeAtDepthAndItsParentIsParagraphWhoseParentIsList($pos: ResolvedPos): boolean {
const depth = $pos.depth;
if (depth >= 0) {
const parent = $pos.node(depth);
const grandParent = $pos.node(depth - 1);
return parent.type.name === "paragraph" && grandParent.type.name === "list";
}
return false;
}

View File

@@ -1,557 +0,0 @@
import { Plugin, EditorState, PluginKey, NodeSelection } from "@tiptap/pm/state";
import { EditorView } from "@tiptap/pm/view";
import { dropPoint } from "@tiptap/pm/transform";
import { Editor, Extension } from "@tiptap/core";
interface DropCursorOptions {
/// The color of the cursor. Defaults to `black`. Use `false` to apply no color and rely only on class.
color?: string | false;
/// The precise width of the cursor in pixels. Defaults to 1.
width?: number;
/// A CSS class name to add to the cursor element.
class?: string;
}
/// Create a plugin that, when added to a ProseMirror instance,
/// causes a decoration to show up at the drop position when something
/// is dragged over the editor.
///
/// Nodes may add a `disableDropCursor` property to their spec to
/// control the showing of a drop cursor inside them. This may be a
/// boolean or a function, which will be called with a view and a
/// position, and should return a boolean.
export function dropCursor(options: DropCursorOptions = {}, tiptapEditorOptions: { editor: Editor }): Plugin {
const pluginKey = new PluginKey("dropCursor");
return new Plugin({
key: pluginKey,
state: {
init() {
return { dropPosByDropCursorPos: null };
},
apply(tr, state) {
// Get the new state from meta
const meta = tr.getMeta(pluginKey);
if (meta) {
return { dropPosByDropCursorPos: meta.dropPosByDropCursorPos };
}
return state;
},
},
view(editorView) {
return new DropCursorView(editorView, options, tiptapEditorOptions.editor, pluginKey);
},
props: {
handleDrop(view, event, slice, moved) {
const { isBetweenFlatLists, isNestedList, hasNestedLists, pos, isHoveringOverListContent } =
// Instead of calling rawIsBetweenFlatListsFn directly, we rely on the
// Plugin's stored value or re-check if needed. But here, you can
// directly call rawIsBetweenFlatListsFn if you wish.
rawIsBetweenFlatListsFn(event, tiptapEditorOptions.editor) || {};
const state = pluginKey.getState(view.state);
let dropPosByDropCursorPos = state?.dropPosByDropCursorPos;
if (isHoveringOverListContent) {
dropPosByDropCursorPos -= 1;
}
if (isBetweenFlatLists && dropPosByDropCursorPos) {
const tr = view.state.tr;
if (moved) {
// Get the size of content to be deleted
const selection = tr.selection;
const deleteSize = selection.to - selection.from;
// Adjust drop position if it's after the deletion point
if (dropPosByDropCursorPos > selection.from) {
dropPosByDropCursorPos -= deleteSize;
}
tr.deleteSelection();
}
const finalDropPos = dropPosByDropCursorPos - 2;
// Insert the content
tr.insert(finalDropPos, slice.content);
// Create a NodeSelection on the newly inserted content
const $pos = tr.doc.resolve(finalDropPos);
const node = $pos.nodeAfter;
if (node) {
const nodeSelection = NodeSelection.create(tr.doc, dropPosByDropCursorPos);
tr.setSelection(nodeSelection);
}
view.dispatch(tr);
return true;
}
return false;
},
},
});
}
// Add disableDropCursor to NodeSpec
declare module "prosemirror-model" {
interface NodeSpec {
disableDropCursor?:
| boolean
| ((view: EditorView, pos: { pos: number; inside: number }, event: DragEvent) => boolean);
}
}
class DropCursorView {
private width: number;
private color: string | undefined;
private class: string | undefined;
private cursorPos: number | null = null;
private element: HTMLElement | null = null;
private timeout: ReturnType<typeof setTimeout> | null = null;
private handlers: { name: string; handler: (event: Event) => void }[];
private editor: Editor;
// Throttled version of our isBetweenFlatListsFn
private isBetweenFlatListsFn: (event: DragEvent) => ReturnType<typeof rawIsBetweenFlatListsFn>;
constructor(
private readonly editorView: EditorView,
options: DropCursorOptions,
editor: Editor,
private readonly pluginKey: PluginKey
) {
this.width = options.width ?? 1;
this.color = options.color === false ? undefined : options.color || `rgb(115, 115, 115)`;
this.class = options.class;
this.editor = editor;
// Create the throttled function and store for use in dragover
this.isBetweenFlatListsFn = createThrottledIsBetweenFlatListsFn(editor);
this.handlers = ["dragover", "dragend", "drop", "dragleave"].map((name) => {
const handler = (e: Event) => {
(this as any)[name](e);
};
editorView.dom.addEventListener(name, handler);
return { name, handler };
});
}
destroy() {
this.handlers.forEach(({ name, handler }) => this.editorView.dom.removeEventListener(name, handler));
}
update(editorView: EditorView, prevState: EditorState) {
if (this.cursorPos != null && prevState.doc != editorView.state.doc) {
if (this.cursorPos > editorView.state.doc.content.size) this.setCursor(null);
else this.updateOverlay();
}
}
setCursor(pos: number | null, isBetweenFlatLists?: boolean) {
this.cursorPos = pos;
if (pos == null) {
if (this.element?.parentNode) {
this.element.parentNode.removeChild(this.element);
}
this.element = null;
} else {
this.updateOverlay(isBetweenFlatLists);
}
}
updateOverlay(isBetweenFlatLists?: boolean) {
const isBetweenFlatList = isBetweenFlatLists ?? false;
const $pos = this.editorView.state.doc.resolve(this.cursorPos!);
const isBlock = !$pos.parent.inlineContent;
let rect: Partial<DOMRect>;
const editorDOM = this.editorView.dom;
const editorRect = editorDOM.getBoundingClientRect();
const scaleX = editorRect.width / editorDOM.offsetWidth;
const scaleY = editorRect.height / editorDOM.offsetHeight;
if (isBlock) {
const before = $pos.nodeBefore;
const after = $pos.nodeAfter;
if (before || after) {
const node = this.editorView.nodeDOM(this.cursorPos! - (before ? before.nodeSize : 0));
if (node) {
const nodeRect = (node as HTMLElement).getBoundingClientRect();
let top = before ? nodeRect.bottom : nodeRect.top;
if (before && after) {
top = (top + (this.editorView.nodeDOM(this.cursorPos!) as HTMLElement).getBoundingClientRect().top) / 2;
}
const halfWidth = (this.width / 2) * scaleY;
rect = {
left: nodeRect.left,
right: nodeRect.right,
top: top - halfWidth,
bottom: top + halfWidth,
};
}
}
}
if (!rect) {
const coords = this.editorView.coordsAtPos(this.cursorPos!);
const halfWidth = (this.width / 2) * scaleX;
rect = {
left: coords.left - halfWidth,
right: coords.left + halfWidth,
top: coords.top,
bottom: coords.bottom,
};
}
const parent = this.editorView.dom.offsetParent as HTMLElement;
if (!this.element) {
this.element = parent.appendChild(document.createElement("div"));
if (this.class) this.element.className = this.class;
this.element.style.cssText = "position: absolute; z-index: 50; pointer-events: none;";
if (this.color) {
this.element.style.backgroundColor = this.color;
}
}
this.element.classList.toggle("prosemirror-dropcursor-block", isBlock);
this.element.classList.toggle("prosemirror-dropcursor-inline", !isBlock);
let parentLeft: number, parentTop: number;
if (!parent || (parent == document.body && getComputedStyle(parent).position == "static")) {
parentLeft = -window.scrollX;
parentTop = -window.scrollY;
} else {
const parentRect = parent.getBoundingClientRect();
const parentScaleX = parentRect.width / parent.offsetWidth;
const parentScaleY = parentRect.height / parent.offsetHeight;
parentLeft = parentRect.left - parent.scrollLeft * parentScaleX;
parentTop = parentRect.top - parent.scrollTop * parentScaleY;
}
// Adjust left if we're between flat lists
const finalLeft = (rect.left! - parentLeft) / scaleX - (isBetweenFlatList ? 20 : 0);
const finalTop = (rect.top! - parentTop) / scaleY;
const finalWidth = (rect.right! - rect.left!) / scaleX;
const finalHeight = (rect.bottom! - rect.top!) / scaleY;
this.element.style.left = finalLeft + "px";
this.element.style.top = finalTop + "px";
this.element.style.width = finalWidth + "px";
this.element.style.height = finalHeight + "px";
}
scheduleRemoval(timeout: number) {
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.setCursor(null), timeout);
}
dragover(event: DragEvent) {
if (!this.editorView.editable) return;
// Throttled call to the function
const result = this.isBetweenFlatListsFn(event);
if (!result) return;
const { isBetweenFlatLists, pos: posList, isHoveringOverListContent } = result;
// If were between flat lists, override the usual pos with posList
const pos = this.editorView.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (!pos) return;
if (isBetweenFlatLists && this.element) {
// Reassign to the pos we discovered in the function
pos.pos = posList;
}
const node = pos.inside >= 0 && this.editorView.state.doc.nodeAt(pos.inside);
const disableDropCursor = node && node.type.spec.disableDropCursor;
const disabled =
typeof disableDropCursor == "function" ? disableDropCursor(this.editorView, pos, event) : disableDropCursor;
if (!disabled && pos) {
let target = pos.pos;
if (this.editorView.dragging && this.editorView.dragging.slice) {
const point = dropPoint(this.editorView.state.doc, target, this.editorView.dragging.slice);
if (point != null) target = point;
}
this.dropPosByDropCursorPos = target;
this.setCursor(target, !!isBetweenFlatLists && !isHoveringOverListContent);
this.scheduleRemoval(5000);
}
}
dragend() {
this.scheduleRemoval(20);
}
drop() {
this.scheduleRemoval(20);
}
dragleave(event: DragEvent) {
const relatedTarget = event.relatedTarget as Node | null;
if (relatedTarget && !this.editorView.dom.contains(relatedTarget)) {
this.setCursor(null);
}
}
set dropPosByDropCursorPos(pos: number | null) {
const tr = this.editorView.state.tr;
tr.setMeta(this.pluginKey, { dropPosByDropCursorPos: pos });
this.editorView.dispatch(tr);
}
get dropPosByDropCursorPos(): number | null {
return this.pluginKey.getState(this.editorView.state)?.dropPosByDropCursorPos;
}
}
export const DropCursorExtension = Extension.create({
name: "dropCursor",
addProseMirrorPlugins() {
return [
dropCursor(
{
width: 2,
class: "transition-all duration-200 ease-[cubic-bezier(0.165, 0.84, 0.44, 1)]",
},
this
),
];
},
});
function rawIsBetweenFlatListsFn(event: DragEvent, editor: Editor) {
// Cache coordinates and use a single object for multiple coordinate lookups
const coords = {
left: event.clientX,
top: event.clientY,
};
// Use WeakMap to cache element positions and calculations
const positionCache = new WeakMap();
// Get element under drag with a more efficient selector strategy
const elementUnderDrag = document.elementFromPoint(coords.left, coords.top);
if (!elementUnderDrag) return null;
// Find closest flat list using a cached selector
const currentFlatList = elementUnderDrag.closest(".prosemirror-flat-list");
if (!currentFlatList) return null;
// Use a single getBoundingClientRect call and cache the result
const currentFlatListRect = currentFlatList.getBoundingClientRect();
// Initialize state object once
const state = {
isHoveringOverListContent: !elementUnderDrag.classList.contains("prosemirror-flat-list"),
isBetweenFlatLists: true,
hasNestedLists: false,
pos: null as number | null,
listLevel: 0,
isNestedList: false,
};
// Efficient position calculation with caching
const getPositionFromElement = (element: Element): number | null => {
if (positionCache.has(element)) {
return positionCache.get(element);
}
const rect = element.getBoundingClientRect();
const pos = editor.view.posAtCoords({
left: rect.left,
top: rect.top,
});
const result = pos?.pos ?? null;
positionCache.set(element, result);
return result;
};
// Batch DOM operations
const sibling = currentFlatList.nextElementSibling;
const firstNestedList = currentFlatList.querySelector(":scope > .prosemirror-flat-list");
// Calculate list level efficiently using a direct parent check
const level = getListLevelOptimized(currentFlatList);
state.listLevel = level;
state.isNestedList = level >= 1;
// Determine position with minimal DOM operations
if (sibling) {
state.pos = getPositionFromElement(sibling);
} else if (firstNestedList) {
state.pos = getPositionFromElement(firstNestedList);
state.hasNestedLists = true;
} else if (level >= 1 && !sibling) {
const parent = currentFlatList.parentElement;
if (parent) {
state.pos = getPositionFromElement(parent);
}
}
if (!state.pos) return null;
return {
...state,
pos: state.pos - 1,
};
}
// Optimized list level calculation
function getListLevelOptimized(element: Element): number {
let level = 0;
let current = element.parentElement;
// Use a more efficient selector matching
while (current && !current.classList.contains("ProseMirror")) {
if (current.classList.contains("prosemirror-flat-list")) {
level++;
}
current = current.parentElement;
}
return level;
}
function getListLevel(element: Element): number {
let level = 0;
let current = element.parentElement;
while (current && current !== document.body) {
if (current.matches(".prosemirror-flat-list")) {
level++;
}
current = current.parentElement;
}
return level;
}
/**
* The original (unthrottled) version of the function that inspects the DOM
* to figure out if we are between flat lists.
*/
// function rawIsBetweenFlatListsFn(event: DragEvent, editor: Editor) {
// const elementUnderDrag = document.elementFromPoint(event.clientX, event.clientY);
// if (!elementUnderDrag) {
// return null;
// }
//
// let editorPos = editor.view.posAtCoords({ left: event.clientX, top: event.clientY });
// let pos = null;
// const currentFlatList = elementUnderDrag.closest(".prosemirror-flat-list");
//
// if (!currentFlatList) {
// return null;
// }
//
// let hasNestedLists = false;
// let firstChild = null;
// let isHoveringOverListContent = false;
//
// // If the element under drag is not the flat list itself but a child of it
// if (currentFlatList && !elementUnderDrag.classList.contains("prosemirror-flat-list")) {
// isHoveringOverListContent = true;
// }
//
// if (currentFlatList) {
// const sibling = currentFlatList.nextElementSibling;
// if (sibling) {
// const rect = sibling.getBoundingClientRect();
// pos = editor.view.posAtCoords({
// left: rect.left,
// top: rect.top,
// });
// }
//
// firstChild = currentFlatList.querySelector(".prosemirror-flat-list") as HTMLElement;
// if (firstChild) {
// const rect = firstChild.getBoundingClientRect();
// pos = editor.view.posAtCoords({
// left: rect.left,
// top: rect.top,
// });
// hasNestedLists = true;
// }
// }
//
// const level = getListLevel(currentFlatList);
// if (level >= 1) {
// const sibling = currentFlatList.nextElementSibling;
// if (!sibling) {
// const currentFlatListParentSibling = currentFlatList.parentElement;
// if (currentFlatListParentSibling) {
// const rect = currentFlatListParentSibling.getBoundingClientRect();
// pos = editor.view.posAtCoords({
// left: rect.left,
// top: rect.top,
// });
// }
// }
// }
//
// if (!pos) {
// return null;
// }
//
// return {
// isHoveringOverListContent,
// isBetweenFlatLists: !!currentFlatList,
// pos: pos.pos - 1,
// listLevel: level,
// isNestedList: level >= 1,
// hasNestedLists,
// };
// }
/**
* Throttler factory for rawIsBetweenFlatListsFn.
* You can tweak timeThreshold and moveThreshold for your needs.
*/
function createThrottledIsBetweenFlatListsFn(
editor: Editor,
timeThreshold = 300, // ms between new computations
moveThreshold = 5 // px of mouse movement before re-checking
) {
let lastCallTime = 0;
let lastX = 0;
let lastY = 0;
let lastResult: ReturnType<typeof rawIsBetweenFlatListsFn> | null = null;
return function throttledIsBetweenFlatListsFn(event: DragEvent) {
const now = performance.now();
const dx = Math.abs(event.clientX - lastX);
const dy = Math.abs(event.clientY - lastY);
// Only recalc if we moved enough OR enough time passed
if (dx < moveThreshold && dy < moveThreshold && now - lastCallTime < timeThreshold) {
return lastResult;
}
lastX = event.clientX;
lastY = event.clientY;
lastCallTime = now;
lastResult = rawIsBetweenFlatListsFn(event, editor);
return lastResult;
};
}
// function getListLevel(element: Element): number {
// let level = 0;
// let current = element.parentElement;
//
// while (current && current !== document.body) {
// if (current.matches(".prosemirror-flat-list")) {
// level++;
// }
// current = current.parentElement;
// }
//
// return level;
// }

View File

@@ -223,7 +223,6 @@ class DropCursorView {
const finalTop = (rect.top! - parentTop) / scaleY;
const finalWidth = (rect.right! - rect.left!) / scaleX;
const finalHeight = (rect.bottom! - rect.top!) / scaleY;
console.log("isBetweenFlatList", isBetweenFlatList);
this.element.style.transform = isBetweenFlatList ? `translateX(${-20}px` : `translateX(0px)`;
this.element.style.left = finalLeft + "px";
this.element.style.top = finalTop + "px";
@@ -334,7 +333,6 @@ function rawIsBetweenFlatListsFn(event: DragEvent, editor: Editor) {
const currentFlatList = elementUnderDrag.closest(".prosemirror-flat-list");
if (!currentFlatList) return null;
console.log(currentFlatList);
let isInsideToggleOrTask = false;
if (
currentFlatList.getAttribute("data-list-kind") === "toggle" ||
@@ -343,7 +341,6 @@ function rawIsBetweenFlatListsFn(event: DragEvent, editor: Editor) {
isInsideToggleOrTask = true;
}
console.log("isInsideToggleOrTask", isInsideToggleOrTask);
const state = {
isHoveringOverListContent: !elementUnderDrag.classList.contains("prosemirror-flat-list"),
isBetweenFlatLists: true,
@@ -357,7 +354,6 @@ function rawIsBetweenFlatListsFn(event: DragEvent, editor: Editor) {
state.isHoveringOverListContent = firstChildListMarker?.classList.contains("list-marker");
}
console.log("isHoveringOverListContent", state.isHoveringOverListContent);
const getPositionFromElement = (element: Element, some?: boolean): number | null => {
if (positionCache.has(element)) {
return positionCache.get(element);
@@ -454,17 +450,3 @@ function createThrottledIsBetweenFlatListsFn(
return lastResult;
};
}
// function getListLevel(element: Element): number {
// let level = 0;
// let current = element.parentElement;
//
// while (current && current !== document.body) {
// if (current.matches(".prosemirror-flat-list")) {
// level++;
// }
// current = current.parentElement;
// }
//
// return level;
// }