Files
tp-tree-nav/mixins/tree-dnd.js
2025-12-28 23:06:37 +01:00

215 lines
6.0 KiB
JavaScript

export const TreeDnDMixin = (superClass) => class TreeDnDMixin extends superClass {
_onDragStart(item, e) {
if (!this.allowDragAndDrop) {
e.preventDefault();
return;
}
this._dragSource = item;
e.dataTransfer.effectAllowed = 'move';
const row = e.currentTarget;
const clone = document.createElement('div');
clone.classList.add('drag-clone');
clone.textContent = row.textContent;
this.shadowRoot.appendChild(clone);
e.dataTransfer.setDragImage(clone, 0, 0);
setTimeout(() => {
clone.remove();
}, 0);
}
_onDragEnd(item, e) {
this._dragSource = null;
}
_onDragOver(item, e) {
if (!this.allowDragAndDrop || !this._dragSource) return;
e.preventDefault();
const row = e.currentTarget;
const rect = row.getBoundingClientRect();
const y = e.clientY - rect.top;
const h = rect.height;
let position = 'inside';
if (y < h * 0.25) position = 'before';
else if (y > h * 0.75) position = 'after';
if (this._dragSource.key === item.key) {
this._clearDragClasses(row);
e.dataTransfer.dropEffect = 'none';
return;
}
if (position === 'after' && item.expanded && item.hasChildren) {
this._clearDragClasses(row);
e.dataTransfer.dropEffect = 'none';
return;
}
const { target: logicalTarget, position: logicalPosition, depth: dragDepth } = this._resolveDropTarget(item, position, e);
if (this._isNoOp(this._dragSource, logicalTarget, logicalPosition)) {
this._clearDragClasses(row);
e.dataTransfer.dropEffect = 'none';
return;
}
if (this.canDrop && !this.canDrop(this._dragSource, logicalTarget, logicalPosition)) {
this._clearDragClasses(row);
e.dataTransfer.dropEffect = 'none';
return;
}
e.dataTransfer.dropEffect = 'move';
this._clearDragClasses(row);
row.classList.add(`drag-over-${position}`);
if (dragDepth !== undefined) {
row.style.setProperty('--drag-depth', dragDepth);
}
}
_onDragLeave(item, e) {
const row = e.currentTarget;
if (row.contains(e.relatedTarget)) return;
this._clearDragClasses(row);
}
_onDrop(item, e) {
if (!this.allowDragAndDrop || !this._dragSource) return;
e.preventDefault();
const row = e.currentTarget;
this._clearDragClasses(row);
const rect = row.getBoundingClientRect();
const y = e.clientY - rect.top;
const h = rect.height;
let position = 'inside';
if (y < h * 0.25) position = 'before';
else if (y > h * 0.75) position = 'after';
if (this._dragSource.key === item.key) return;
if (position === 'after' && item.expanded && item.hasChildren) {
return;
}
const { target: logicalTarget, position: logicalPosition } = this._resolveDropTarget(item, position, e);
if (this._isNoOp(this._dragSource, logicalTarget, logicalPosition)) {
return;
}
if (this.canDrop && !this.canDrop(this._dragSource, logicalTarget, logicalPosition)) {
return;
}
this.dispatchEvent(new CustomEvent('node-drop', {
detail: {
source: this._dragSource,
target: logicalTarget,
position: logicalPosition,
originalEvent: e
},
bubbles: true,
composed: true
}));
this._dragSource = null;
}
_isNoOp(source, target, position) {
if (!source || !target) return true;
if (source.key === target.key) return true;
const srcPathStr = source.path.slice(0, -1).join('/');
const tgtPathStr = target.path.slice(0, -1).join('/');
if (srcPathStr !== tgtPathStr) return false;
const siblings = this._getSiblings(source.path);
if (!siblings) return false;
const srcIdx = siblings.indexOf(source.node);
const tgtIdx = siblings.indexOf(target.node);
if (srcIdx === -1 || tgtIdx === -1) return false;
if (position === 'before') {
if (srcIdx === tgtIdx - 1) return true;
}
if (position === 'after') {
if (srcIdx === tgtIdx + 1) return true;
}
return false;
}
_getSiblings(path) {
if (path.length === 1) return this.roots;
let nodes = this.roots;
for (let i = 0; i < path.length - 1; i++) {
const slug = path[i];
const node = nodes.find((n, idx) => (n.slug ?? `${idx}`) === slug);
if (!node) return null;
nodes = node.children || [];
}
return nodes;
}
_resolveDropTarget(item, position, e) {
if (!this._flatItems) return { target: item, position };
const index = this._flatItems.findIndex(i => i.key === item.key);
if (index === -1) return { target: item, position };
const currentItem = this._flatItems[index];
if (position === 'after' && e) {
const nextItem = this._flatItems[index + 1];
const currentDepth = currentItem.depth;
const nextDepth = nextItem ? nextItem.depth : -1;
if (nextDepth < currentDepth) {
const row = e.currentTarget;
const style = getComputedStyle(row);
const indentVal = style.getPropertyValue('--tp-tree-indent').trim();
const indent = indentVal ? parseInt(indentVal, 10) : 16;
const padding = parseFloat(style.paddingLeft) || 8;
const rowRect = row.getBoundingClientRect();
const mouseX = e.clientX - rowRect.left;
let targetDepth = Math.floor((mouseX - padding) / indent);
if (targetDepth < nextDepth) targetDepth = nextDepth;
if (targetDepth > currentDepth) targetDepth = currentDepth;
if (targetDepth < currentDepth) {
let ancestor = currentItem;
for (let i = index; i >= 0; i--) {
if (this._flatItems[i].depth === targetDepth) {
ancestor = this._flatItems[i];
break;
}
}
return { target: ancestor, position: 'after', depth: targetDepth };
}
}
}
return { target: currentItem, position };
}
_clearDragClasses(row) {
row.classList.remove('drag-over-inside', 'drag-over-before', 'drag-over-after');
row.style.removeProperty('--drag-depth');
}
};