mirror of
https://github.com/makeplane/plane.git
synced 2025-12-25 08:09:33 +01:00
[PE-101] feat: smooth scrolling in editor while dragging and dropping nodes (#6233)
* fix: smoother drag scrolling * fix: refactoring out common fns * fix: moved to mouse events instead of drag * fix: improving the drag preview * fix: added better selection logic * fix: drag handle new way almost working * fix: drag-handle old behaviour with better scrolling * fix: remove experiments * fix: better scroll thresholds * fix: transition to drop cursor added * fix: drag handling speed * fix: cleaning up listeners * fix: common out selection and dragging logic * fix: scroll threshold logic fixed
This commit is contained in:
@@ -82,7 +82,8 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
},
|
||||
},
|
||||
dropcursor: {
|
||||
class: "text-custom-text-300",
|
||||
class:
|
||||
"text-custom-text-300 transition-all motion-reduce:transition-none motion-reduce:hover:transform-none duration-200 ease-[cubic-bezier(0.165, 0.84, 0.44, 1)]",
|
||||
},
|
||||
...(enableHistory ? {} : { history: false }),
|
||||
}),
|
||||
|
||||
@@ -42,7 +42,7 @@ export const SideMenuExtension = (props: Props) => {
|
||||
ai: aiEnabled,
|
||||
dragDrop: dragDropEnabled,
|
||||
},
|
||||
scrollThreshold: { up: 200, down: 100 },
|
||||
scrollThreshold: { up: 200, down: 150 },
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Fragment, Slice, Node } from "@tiptap/pm/model";
|
||||
import { NodeSelection, TextSelection } from "@tiptap/pm/state";
|
||||
import { Fragment, Slice, Node, Schema } from "@tiptap/pm/model";
|
||||
import { NodeSelection } from "@tiptap/pm/state";
|
||||
// @ts-expect-error __serializeForClipboard's is not exported
|
||||
import { __serializeForClipboard, EditorView } from "@tiptap/pm/view";
|
||||
// extensions
|
||||
@@ -8,6 +8,29 @@ import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions";
|
||||
const verticalEllipsisIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis-vertical"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>';
|
||||
|
||||
const generalSelectors = [
|
||||
"li",
|
||||
"p:not(:first-child)",
|
||||
".code-block",
|
||||
"blockquote",
|
||||
"h1, h2, h3, h4, h5, h6",
|
||||
"[data-type=horizontalRule]",
|
||||
".table-wrapper",
|
||||
".issue-embed",
|
||||
".image-component",
|
||||
".image-upload-component",
|
||||
".editor-callout-component",
|
||||
].join(", ");
|
||||
|
||||
const maxScrollSpeed = 20;
|
||||
const acceleration = 0.5;
|
||||
|
||||
const scrollParentCache = new WeakMap();
|
||||
|
||||
function easeOutQuadAnimation(t: number) {
|
||||
return t * (2 - t);
|
||||
}
|
||||
|
||||
const createDragHandleElement = (): HTMLElement => {
|
||||
const dragHandleElement = document.createElement("button");
|
||||
dragHandleElement.type = "button";
|
||||
@@ -30,21 +53,39 @@ const createDragHandleElement = (): HTMLElement => {
|
||||
return dragHandleElement;
|
||||
};
|
||||
|
||||
const isScrollable = (node: HTMLElement | SVGElement) => {
|
||||
if (!(node instanceof HTMLElement || node instanceof SVGElement)) {
|
||||
return false;
|
||||
}
|
||||
const style = getComputedStyle(node);
|
||||
return ["overflow", "overflow-y"].some((propertyName) => {
|
||||
const value = style.getPropertyValue(propertyName);
|
||||
return value === "auto" || value === "scroll";
|
||||
});
|
||||
};
|
||||
|
||||
const getScrollParent = (node: HTMLElement | SVGElement) => {
|
||||
if (scrollParentCache.has(node)) {
|
||||
return scrollParentCache.get(node);
|
||||
}
|
||||
|
||||
let currentParent = node.parentElement;
|
||||
|
||||
while (currentParent) {
|
||||
if (isScrollable(currentParent)) {
|
||||
scrollParentCache.set(node, currentParent);
|
||||
return currentParent;
|
||||
}
|
||||
currentParent = currentParent.parentElement;
|
||||
}
|
||||
|
||||
const result = document.scrollingElement || document.documentElement;
|
||||
scrollParentCache.set(node, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
||||
const elements = document.elementsFromPoint(coords.x, coords.y);
|
||||
const generalSelectors = [
|
||||
"li",
|
||||
"p:not(:first-child)",
|
||||
".code-block",
|
||||
"blockquote",
|
||||
"h1, h2, h3, h4, h5, h6",
|
||||
"[data-type=horizontalRule]",
|
||||
".table-wrapper",
|
||||
".issue-embed",
|
||||
".image-component",
|
||||
".image-upload-component",
|
||||
".editor-callout-component",
|
||||
].join(", ");
|
||||
|
||||
for (const elem of elements) {
|
||||
if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) {
|
||||
@@ -85,140 +126,73 @@ const nodePosAtDOMForBlockQuotes = (node: Element, view: EditorView) => {
|
||||
})?.inside;
|
||||
};
|
||||
|
||||
const calcNodePos = (pos: number, view: EditorView, node: Element) => {
|
||||
const maxPos = view.state.doc.content.size;
|
||||
const safePos = Math.max(0, Math.min(pos, maxPos));
|
||||
const $pos = view.state.doc.resolve(safePos);
|
||||
|
||||
if ($pos.depth > 1) {
|
||||
if (node.matches("ul li, ol li")) {
|
||||
// only for nested lists
|
||||
const newPos = $pos.before($pos.depth);
|
||||
return Math.max(0, Math.min(newPos, maxPos));
|
||||
}
|
||||
}
|
||||
|
||||
return safePos;
|
||||
};
|
||||
|
||||
export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOptions => {
|
||||
let listType = "";
|
||||
const handleDragStart = (event: DragEvent, view: EditorView) => {
|
||||
view.focus();
|
||||
|
||||
if (!event.dataTransfer) return;
|
||||
|
||||
const node = nodeDOMAtCoords({
|
||||
x: event.clientX + 50 + options.dragHandleWidth,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
if (!(node instanceof Element)) return;
|
||||
|
||||
let draggedNodePos = nodePosAtDOM(node, view, options);
|
||||
if (draggedNodePos == null || draggedNodePos < 0) return;
|
||||
draggedNodePos = calcNodePos(draggedNodePos, view, node);
|
||||
|
||||
const { from, to } = view.state.selection;
|
||||
const diff = from - to;
|
||||
|
||||
const fromSelectionPos = calcNodePos(from, view, node);
|
||||
let differentNodeSelected = false;
|
||||
|
||||
const nodePos = view.state.doc.resolve(fromSelectionPos);
|
||||
|
||||
// Check if nodePos points to the top level node
|
||||
if (nodePos.node().type.name === "doc") differentNodeSelected = true;
|
||||
else {
|
||||
// TODO FIX ERROR
|
||||
const nodeSelection = NodeSelection.create(view.state.doc, nodePos.before());
|
||||
// Check if the node where the drag event started is part of the current selection
|
||||
differentNodeSelected = !(
|
||||
draggedNodePos + 1 >= nodeSelection.$from.pos && draggedNodePos <= nodeSelection.$to.pos
|
||||
);
|
||||
}
|
||||
|
||||
if (!differentNodeSelected && diff !== 0 && !(view.state.selection instanceof NodeSelection)) {
|
||||
const endSelection = NodeSelection.create(view.state.doc, to - 1);
|
||||
const multiNodeSelection = TextSelection.create(view.state.doc, draggedNodePos, endSelection.$to.pos);
|
||||
view.dispatch(view.state.tr.setSelection(multiNodeSelection));
|
||||
} else {
|
||||
// TODO FIX ERROR
|
||||
const nodeSelection = NodeSelection.create(view.state.doc, draggedNodePos);
|
||||
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
||||
}
|
||||
|
||||
// If the selected node is a list item, we need to save the type of the wrapping list e.g. OL or UL
|
||||
if (view.state.selection instanceof NodeSelection && view.state.selection.node.type.name === "listItem") {
|
||||
listType = node.parentElement!.tagName;
|
||||
}
|
||||
|
||||
if (node.matches("blockquote")) {
|
||||
let nodePosForBlockQuotes = nodePosAtDOMForBlockQuotes(node, view);
|
||||
if (nodePosForBlockQuotes === null || nodePosForBlockQuotes === undefined) return;
|
||||
|
||||
const docSize = view.state.doc.content.size;
|
||||
nodePosForBlockQuotes = Math.max(0, Math.min(nodePosForBlockQuotes, docSize));
|
||||
|
||||
if (nodePosForBlockQuotes >= 0 && nodePosForBlockQuotes <= docSize) {
|
||||
// TODO FIX ERROR
|
||||
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockQuotes);
|
||||
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
||||
}
|
||||
}
|
||||
|
||||
const slice = view.state.selection.content();
|
||||
const { dom, text } = __serializeForClipboard(view, slice);
|
||||
|
||||
event.dataTransfer.clearData();
|
||||
event.dataTransfer.setData("text/html", dom.innerHTML);
|
||||
event.dataTransfer.setData("text/plain", text);
|
||||
event.dataTransfer.effectAllowed = "copyMove";
|
||||
|
||||
event.dataTransfer.setDragImage(node, 0, 0);
|
||||
|
||||
view.dragging = { slice, move: event.ctrlKey };
|
||||
};
|
||||
let isDragging = false;
|
||||
let lastClientY = 0;
|
||||
let scrollAnimationFrame = null;
|
||||
let isDraggedOutsideWindow: "top" | "bottom" | boolean = false;
|
||||
let isMouseInsideWhileDragging = false;
|
||||
let currentScrollSpeed = 0;
|
||||
|
||||
const handleClick = (event: MouseEvent, view: EditorView) => {
|
||||
view.focus();
|
||||
handleNodeSelection(event, view, false, options);
|
||||
};
|
||||
|
||||
const node = nodeDOMAtCoords({
|
||||
x: event.clientX + 50 + options.dragHandleWidth,
|
||||
y: event.clientY,
|
||||
});
|
||||
const handleDragStart = (event: DragEvent, view: EditorView) => {
|
||||
const { listType: listTypeFromDragStart } = handleNodeSelection(event, view, true, options);
|
||||
listType = listTypeFromDragStart;
|
||||
isDragging = true;
|
||||
lastClientY = event.clientY;
|
||||
scroll();
|
||||
};
|
||||
|
||||
if (!(node instanceof Element)) return;
|
||||
const handleDragEnd = <TEvent extends DragEvent | FocusEvent>(event: TEvent, view?: EditorView) => {
|
||||
event.preventDefault();
|
||||
isDragging = false;
|
||||
isMouseInsideWhileDragging = false;
|
||||
if (scrollAnimationFrame) {
|
||||
cancelAnimationFrame(scrollAnimationFrame);
|
||||
scrollAnimationFrame = null;
|
||||
}
|
||||
|
||||
if (node.matches("blockquote")) {
|
||||
let nodePosForBlockQuotes = nodePosAtDOMForBlockQuotes(node, view);
|
||||
if (nodePosForBlockQuotes === null || nodePosForBlockQuotes === undefined) return;
|
||||
view?.dom.classList.remove("dragging");
|
||||
};
|
||||
|
||||
const docSize = view.state.doc.content.size;
|
||||
nodePosForBlockQuotes = Math.max(0, Math.min(nodePosForBlockQuotes, docSize));
|
||||
|
||||
if (nodePosForBlockQuotes >= 0 && nodePosForBlockQuotes <= docSize) {
|
||||
// TODO FIX ERROR
|
||||
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockQuotes);
|
||||
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
||||
}
|
||||
function scroll() {
|
||||
if (!isDragging) {
|
||||
currentScrollSpeed = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
let nodePos = nodePosAtDOM(node, view, options);
|
||||
const scrollableParent = getScrollParent(dragHandleElement);
|
||||
if (!scrollableParent) return;
|
||||
|
||||
if (nodePos === null || nodePos === undefined) return;
|
||||
const scrollRegionUp = options.scrollThreshold.up;
|
||||
const scrollRegionDown = window.innerHeight - options.scrollThreshold.down;
|
||||
|
||||
// Adjust the nodePos to point to the start of the node, ensuring NodeSelection can be applied
|
||||
nodePos = calcNodePos(nodePos, view, node);
|
||||
let targetScrollAmount = 0;
|
||||
|
||||
// TODO FIX ERROR
|
||||
// Use NodeSelection to select the node at the calculated position
|
||||
const nodeSelection = NodeSelection.create(view.state.doc, nodePos);
|
||||
if (isDraggedOutsideWindow === "top") {
|
||||
targetScrollAmount = -maxScrollSpeed * 5;
|
||||
} else if (isDraggedOutsideWindow === "bottom") {
|
||||
targetScrollAmount = maxScrollSpeed * 5;
|
||||
} else if (lastClientY < scrollRegionUp) {
|
||||
const ratio = easeOutQuadAnimation((scrollRegionUp - lastClientY) / options.scrollThreshold.up);
|
||||
targetScrollAmount = -maxScrollSpeed * ratio;
|
||||
} else if (lastClientY > scrollRegionDown) {
|
||||
const ratio = easeOutQuadAnimation((lastClientY - scrollRegionDown) / options.scrollThreshold.down);
|
||||
targetScrollAmount = maxScrollSpeed * ratio;
|
||||
}
|
||||
|
||||
// Dispatch the transaction to update the selection
|
||||
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
||||
};
|
||||
currentScrollSpeed += (targetScrollAmount - currentScrollSpeed) * acceleration;
|
||||
|
||||
if (Math.abs(currentScrollSpeed) > 0.1) {
|
||||
scrollableParent.scrollBy({ top: currentScrollSpeed });
|
||||
}
|
||||
|
||||
scrollAnimationFrame = requestAnimationFrame(scroll);
|
||||
}
|
||||
|
||||
let dragHandleElement: HTMLElement | null = null;
|
||||
// drag handle view actions
|
||||
@@ -231,51 +205,46 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp
|
||||
const view = (view: EditorView, sideMenu: HTMLDivElement | null) => {
|
||||
dragHandleElement = createDragHandleElement();
|
||||
dragHandleElement.addEventListener("dragstart", (e) => handleDragStart(e, view));
|
||||
dragHandleElement.addEventListener("dragend", (e) => handleDragEnd(e, view));
|
||||
dragHandleElement.addEventListener("click", (e) => handleClick(e, view));
|
||||
dragHandleElement.addEventListener("contextmenu", (e) => handleClick(e, view));
|
||||
|
||||
const isScrollable = (node: HTMLElement | SVGElement) => {
|
||||
if (!(node instanceof HTMLElement || node instanceof SVGElement)) {
|
||||
return false;
|
||||
const dragOverHandler = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
if (isDragging) {
|
||||
lastClientY = e.clientY;
|
||||
}
|
||||
const style = getComputedStyle(node);
|
||||
return ["overflow", "overflow-y"].some((propertyName) => {
|
||||
const value = style.getPropertyValue(propertyName);
|
||||
return value === "auto" || value === "scroll";
|
||||
});
|
||||
};
|
||||
|
||||
const getScrollParent = (node: HTMLElement | SVGElement) => {
|
||||
let currentParent = node.parentElement;
|
||||
while (currentParent) {
|
||||
if (isScrollable(currentParent)) {
|
||||
return currentParent;
|
||||
const mouseMoveHandler = (e: MouseEvent) => {
|
||||
if (isMouseInsideWhileDragging) {
|
||||
handleDragEnd(e, view);
|
||||
}
|
||||
};
|
||||
|
||||
const dragLeaveHandler = (e: DragEvent) => {
|
||||
if (e.clientY <= 0 || e.clientX <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
||||
isMouseInsideWhileDragging = true;
|
||||
|
||||
const windowMiddleY = window.innerHeight / 2;
|
||||
|
||||
if (lastClientY < windowMiddleY) {
|
||||
isDraggedOutsideWindow = "top";
|
||||
} else {
|
||||
isDraggedOutsideWindow = "bottom";
|
||||
}
|
||||
currentParent = currentParent.parentElement;
|
||||
}
|
||||
return document.scrollingElement || document.documentElement;
|
||||
};
|
||||
|
||||
const maxScrollSpeed = 100;
|
||||
const dragEnterHandler = () => {
|
||||
isDraggedOutsideWindow = false;
|
||||
};
|
||||
|
||||
dragHandleElement.addEventListener("drag", (e) => {
|
||||
hideDragHandle();
|
||||
const scrollableParent = getScrollParent(dragHandleElement);
|
||||
if (!scrollableParent) return;
|
||||
const scrollThreshold = options.scrollThreshold;
|
||||
window.addEventListener("dragleave", dragLeaveHandler);
|
||||
window.addEventListener("dragenter", dragEnterHandler);
|
||||
|
||||
if (e.clientY < scrollThreshold.up) {
|
||||
const overflow = scrollThreshold.up - e.clientY;
|
||||
const ratio = Math.min(overflow / scrollThreshold.up, 1);
|
||||
const scrollAmount = -maxScrollSpeed * ratio;
|
||||
scrollableParent.scrollBy({ top: scrollAmount });
|
||||
} else if (window.innerHeight - e.clientY < scrollThreshold.down) {
|
||||
const overflow = e.clientY - (window.innerHeight - scrollThreshold.down);
|
||||
const ratio = Math.min(overflow / scrollThreshold.down, 1);
|
||||
const scrollAmount = maxScrollSpeed * ratio;
|
||||
scrollableParent.scrollBy({ top: scrollAmount });
|
||||
}
|
||||
});
|
||||
document.addEventListener("dragover", dragOverHandler);
|
||||
document.addEventListener("mousemove", mouseMoveHandler);
|
||||
|
||||
hideDragHandle();
|
||||
|
||||
@@ -285,6 +254,15 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp
|
||||
destroy: () => {
|
||||
dragHandleElement?.remove?.();
|
||||
dragHandleElement = null;
|
||||
isDragging = false;
|
||||
if (scrollAnimationFrame) {
|
||||
cancelAnimationFrame(scrollAnimationFrame);
|
||||
scrollAnimationFrame = null;
|
||||
}
|
||||
window.removeEventListener("dragleave", dragLeaveHandler);
|
||||
window.removeEventListener("dragenter", dragEnterHandler);
|
||||
document.removeEventListener("dragover", dragOverHandler);
|
||||
document.removeEventListener("mousemove", mouseMoveHandler);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -313,29 +291,36 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp
|
||||
|
||||
const resolvedPos = view.state.doc.resolve(dropPos.pos);
|
||||
let isDroppedInsideList = false;
|
||||
let dropDepth = 0;
|
||||
|
||||
// Traverse up the document tree to find if we're inside a list item
|
||||
for (let i = resolvedPos.depth; i > 0; i--) {
|
||||
if (resolvedPos.node(i).type.name === "listItem") {
|
||||
isDroppedInsideList = true;
|
||||
dropDepth = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the selected node is a list item and is not dropped inside a list, we need to wrap it inside <ol> tag otherwise ol list items will be transformed into ul list item when dropped
|
||||
if (
|
||||
view.state.selection instanceof NodeSelection &&
|
||||
view.state.selection.node.type.name === "listItem" &&
|
||||
!isDroppedInsideList &&
|
||||
listType == "OL"
|
||||
) {
|
||||
const text = droppedNode.textContent;
|
||||
if (!text) return;
|
||||
const paragraph = view.state.schema.nodes.paragraph?.createAndFill({}, view.state.schema.text(text));
|
||||
const listItem = view.state.schema.nodes.listItem?.createAndFill({}, paragraph);
|
||||
// Handle nested list items and task items
|
||||
if (droppedNode.type.name === "listItem") {
|
||||
let slice = view.state.selection.content();
|
||||
let newFragment = slice.content;
|
||||
|
||||
const newList = view.state.schema.nodes.orderedList?.createAndFill(null, listItem);
|
||||
const slice = new Slice(Fragment.from(newList), 0, 0);
|
||||
// If dropping outside a list or at a different depth, adjust the structure
|
||||
if (!isDroppedInsideList || dropDepth !== resolvedPos.depth) {
|
||||
// Flatten the structure if needed
|
||||
newFragment = flattenListStructure(newFragment, view.state.schema);
|
||||
}
|
||||
|
||||
// Wrap in appropriate list type if dropped outside a list
|
||||
if (!isDroppedInsideList) {
|
||||
const listNodeType =
|
||||
listType === "OL" ? view.state.schema.nodes.orderedList : view.state.schema.nodes.bulletList;
|
||||
newFragment = Fragment.from(listNodeType.create(null, newFragment));
|
||||
}
|
||||
|
||||
slice = new Slice(newFragment, slice.openStart, slice.openEnd);
|
||||
view.dragging = { slice, move: event.ctrlKey };
|
||||
}
|
||||
},
|
||||
@@ -349,3 +334,90 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp
|
||||
domEvents,
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to flatten nested list structure
|
||||
function flattenListStructure(fragment: Fragment, schema: Schema): Fragment {
|
||||
const result: Node[] = [];
|
||||
fragment.forEach((node) => {
|
||||
if (node.type === schema.nodes.listItem || node.type === schema.nodes.taskItem) {
|
||||
result.push(node);
|
||||
if (
|
||||
node.content.firstChild &&
|
||||
(node.content.firstChild.type === schema.nodes.bulletList ||
|
||||
node.content.firstChild.type === schema.nodes.orderedList)
|
||||
) {
|
||||
const sublist = node.content.firstChild;
|
||||
const flattened = flattenListStructure(sublist.content, schema);
|
||||
flattened.forEach((subNode) => result.push(subNode));
|
||||
}
|
||||
}
|
||||
});
|
||||
return Fragment.from(result);
|
||||
}
|
||||
|
||||
const handleNodeSelection = (
|
||||
event: MouseEvent | DragEvent,
|
||||
view: EditorView,
|
||||
isDragStart: boolean,
|
||||
options: SideMenuPluginProps
|
||||
) => {
|
||||
let listType = "";
|
||||
view.focus();
|
||||
|
||||
const node = nodeDOMAtCoords({
|
||||
x: event.clientX + 50 + options.dragHandleWidth,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
if (!(node instanceof Element)) return;
|
||||
|
||||
let draggedNodePos = nodePosAtDOM(node, view, options);
|
||||
if (draggedNodePos == null || draggedNodePos < 0) return;
|
||||
|
||||
// Handle blockquotes separately
|
||||
if (node.matches("blockquote")) {
|
||||
draggedNodePos = nodePosAtDOMForBlockQuotes(node, view);
|
||||
if (draggedNodePos === null || draggedNodePos === undefined) return;
|
||||
} else {
|
||||
// Resolve the position to get the parent node
|
||||
const $pos = view.state.doc.resolve(draggedNodePos);
|
||||
|
||||
// If it's a nested list item or task item, move up to the item level
|
||||
if (($pos.parent.type.name === "listItem" || $pos.parent.type.name === "taskItem") && $pos.depth > 1) {
|
||||
draggedNodePos = $pos.before($pos.depth);
|
||||
}
|
||||
}
|
||||
|
||||
const docSize = view.state.doc.content.size;
|
||||
draggedNodePos = Math.max(0, Math.min(draggedNodePos, docSize));
|
||||
|
||||
// Use NodeSelection to select the node at the calculated position
|
||||
const nodeSelection = NodeSelection.create(view.state.doc, draggedNodePos);
|
||||
|
||||
// Dispatch the transaction to update the selection
|
||||
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
||||
|
||||
if (isDragStart) {
|
||||
// Additional logic for drag start
|
||||
if (event instanceof DragEvent && !event.dataTransfer) return;
|
||||
|
||||
if (nodeSelection.node.type.name === "listItem" || nodeSelection.node.type.name === "taskItem") {
|
||||
listType = node.closest("ol, ul")?.tagName || "";
|
||||
}
|
||||
|
||||
const slice = view.state.selection.content();
|
||||
const { dom, text } = __serializeForClipboard(view, slice);
|
||||
|
||||
if (event instanceof DragEvent) {
|
||||
event.dataTransfer.clearData();
|
||||
event.dataTransfer.setData("text/html", dom.innerHTML);
|
||||
event.dataTransfer.setData("text/plain", text);
|
||||
event.dataTransfer.effectAllowed = "copyMove";
|
||||
event.dataTransfer.setDragImage(node, 0, 0);
|
||||
}
|
||||
|
||||
view.dragging = { slice, move: event.ctrlKey };
|
||||
}
|
||||
|
||||
return { listType };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user