mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
chore: remove testing code
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 we’re 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;
|
||||
// }
|
||||
@@ -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;
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user