mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
239 lines
6.4 KiB
TypeScript
239 lines
6.4 KiB
TypeScript
|
import { Extension } from "@tiptap/core";
|
||
|
|
||
|
import { NodeSelection, Plugin } from "@tiptap/pm/state";
|
||
|
// @ts-ignore
|
||
|
import { __serializeForClipboard, EditorView } from "@tiptap/pm/view";
|
||
|
|
||
|
export interface DragHandleOptions {
|
||
|
dragHandleWidth: number;
|
||
|
}
|
||
|
function absoluteRect(node: Element) {
|
||
|
const data = node.getBoundingClientRect();
|
||
|
|
||
|
return {
|
||
|
top: data.top,
|
||
|
left: data.left,
|
||
|
width: data.width,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function nodeDOMAtCoords(coords: { x: number; y: number }) {
|
||
|
return document
|
||
|
.elementsFromPoint(coords.x, coords.y)
|
||
|
.find(
|
||
|
(elem: Element) =>
|
||
|
elem.parentElement?.matches?.(".ProseMirror") ||
|
||
|
elem.matches(
|
||
|
[
|
||
|
"li",
|
||
|
"p:not(:first-child)",
|
||
|
"pre",
|
||
|
"blockquote",
|
||
|
"h1, h2, h3, h4, h5, h6",
|
||
|
"tr",
|
||
|
"th",
|
||
|
"td"
|
||
|
].join(", ")
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function nodePosAtDOM(node: Element, view: EditorView) {
|
||
|
const boundingRect = node.getBoundingClientRect();
|
||
|
|
||
|
return view.posAtCoords({
|
||
|
left: boundingRect.left + 1,
|
||
|
top: boundingRect.top + 1,
|
||
|
})?.inside;
|
||
|
}
|
||
|
|
||
|
function DragHandle(options: DragHandleOptions) {
|
||
|
function 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;
|
||
|
|
||
|
// const nodePos = view.posAtCoords({ left: event.clientX + 50 + options.dragHandleWidth, top: event.clientY })?.inside;
|
||
|
let nodePos = view.posAtCoords({ left: event.clientX + 50 + options.dragHandleWidth, top: event.clientY })?.inside;
|
||
|
|
||
|
if (nodePos === null) {
|
||
|
// Try positions to the right of the given coordinates
|
||
|
const offsets = [1, 2, 3, 4, 5];
|
||
|
for (const offset of offsets) {
|
||
|
const pos = view.posAtCoords({ left: event.clientX + 50 + options.dragHandleWidth + offset, top: event.clientY })?.inside;
|
||
|
if (pos !== null && view.state.doc.nodeAt(pos as number) !== null) {
|
||
|
nodePos = pos;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// const nodePos = nodePosAtDOM(node, view);
|
||
|
if (!nodePos || nodePos < 0) return;
|
||
|
|
||
|
view.dispatch(
|
||
|
view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos))
|
||
|
);
|
||
|
|
||
|
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 };
|
||
|
}
|
||
|
|
||
|
function handleClick(event: MouseEvent, view: EditorView) {
|
||
|
view.focus();
|
||
|
|
||
|
view.dom.classList.remove("dragging");
|
||
|
|
||
|
const node = nodeDOMAtCoords({
|
||
|
x: event.clientX + 50 + options.dragHandleWidth,
|
||
|
y: event.clientY,
|
||
|
});
|
||
|
|
||
|
if (!(node instanceof Element)) return;
|
||
|
|
||
|
// const nodePos = view.posAtCoords({ left: event.clientX + 50 + options.dragHandleWidth, top: event.clientY })?.inside;
|
||
|
// console.log("sadfa", pos)
|
||
|
const nodePos = nodePosAtDOM(node, view);
|
||
|
if (!nodePos || nodePos < 0) return;
|
||
|
console.log('nodePos:', nodePos);
|
||
|
console.log('content at nodePos:', view.state.doc.nodeAt(nodePos));
|
||
|
|
||
|
const parentPos = view.state.doc.resolve(nodePos).parentOffset;
|
||
|
const parentNode = view.state.doc.nodeAt(parentPos);
|
||
|
|
||
|
console.log('parentNode:', parentNode);
|
||
|
console.log('parentNode content expression:', parentNode?.type);
|
||
|
|
||
|
view.dispatch(
|
||
|
view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos))
|
||
|
);
|
||
|
}
|
||
|
|
||
|
let dragHandleElement: HTMLElement | null = null;
|
||
|
|
||
|
function hideDragHandle() {
|
||
|
if (dragHandleElement) {
|
||
|
dragHandleElement.classList.add("hidden");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function showDragHandle() {
|
||
|
if (dragHandleElement) {
|
||
|
dragHandleElement.classList.remove("hidden");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return new Plugin({
|
||
|
view: (view) => {
|
||
|
dragHandleElement = document.createElement("div");
|
||
|
dragHandleElement.draggable = true;
|
||
|
dragHandleElement.dataset.dragHandle = "";
|
||
|
dragHandleElement.classList.add("drag-handle");
|
||
|
dragHandleElement.addEventListener("dragstart", (e) => {
|
||
|
handleDragStart(e, view);
|
||
|
});
|
||
|
dragHandleElement.addEventListener("click", (e) => {
|
||
|
handleClick(e, view);
|
||
|
});
|
||
|
|
||
|
hideDragHandle();
|
||
|
|
||
|
view?.dom?.parentElement?.appendChild(dragHandleElement);
|
||
|
|
||
|
return {
|
||
|
destroy: () => {
|
||
|
dragHandleElement?.remove?.();
|
||
|
dragHandleElement = null;
|
||
|
},
|
||
|
};
|
||
|
},
|
||
|
props: {
|
||
|
handleDOMEvents: {
|
||
|
mousemove: (view, event) => {
|
||
|
if (!view.editable) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const node = nodeDOMAtCoords({
|
||
|
x: event.clientX + options.dragHandleWidth,
|
||
|
y: event.clientY,
|
||
|
});
|
||
|
|
||
|
if (!(node instanceof Element)) {
|
||
|
hideDragHandle();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const compStyle = window.getComputedStyle(node);
|
||
|
const lineHeight = parseInt(compStyle.lineHeight, 10);
|
||
|
const paddingTop = parseInt(compStyle.paddingTop, 10);
|
||
|
|
||
|
const rect = absoluteRect(node);
|
||
|
|
||
|
rect.top += (lineHeight - 24) / 2;
|
||
|
rect.top += paddingTop;
|
||
|
// Li markers
|
||
|
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
||
|
rect.left -= options.dragHandleWidth;
|
||
|
}
|
||
|
rect.width = options.dragHandleWidth;
|
||
|
|
||
|
if (!dragHandleElement) return;
|
||
|
|
||
|
dragHandleElement.style.left = `${rect.left - rect.width}px`;
|
||
|
dragHandleElement.style.top = `${rect.top}px`;
|
||
|
showDragHandle();
|
||
|
},
|
||
|
keydown: () => {
|
||
|
hideDragHandle();
|
||
|
},
|
||
|
mousewheel: () => {
|
||
|
hideDragHandle();
|
||
|
},
|
||
|
// dragging class is used for CSS
|
||
|
dragstart: (view) => {
|
||
|
view.dom.classList.add("dragging");
|
||
|
},
|
||
|
drop: (view) => {
|
||
|
view.dom.classList.remove("dragging");
|
||
|
},
|
||
|
dragend: (view) => {
|
||
|
view.dom.classList.remove("dragging");
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
interface DragAndDropOptions { }
|
||
|
|
||
|
const DragAndDrop = Extension.create<DragAndDropOptions>({
|
||
|
name: "dragAndDrop",
|
||
|
|
||
|
addProseMirrorPlugins() {
|
||
|
return [
|
||
|
DragHandle({
|
||
|
dragHandleWidth: 24,
|
||
|
}),
|
||
|
];
|
||
|
},
|
||
|
});
|
||
|
|
||
|
export default DragAndDrop;
|