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'); } };