/** * Minimal hook for HTML5 drag-and-drop events. * Only captures events and pushes to LiveView - no business logic. */ interface DragData { type: "site" | "device" id: string siteId: string position: number } export const DeviceListReorder = { draggedElement: null as HTMLElement | null, dragData: null as DragData | null, handleDragStart: null as ((e: DragEvent) => void) | null, handleDragEnd: null as ((e: DragEvent) => void) | null, handleDragOver: null as ((e: DragEvent) => void) | null, handleDrop: null as ((e: DragEvent) => void) | null, dragHandleListeners: [] as Array<{ element: Element; handler: (e: Event) => void }>, mounted(this: any) { // Capture drag start this.handleDragStart = (e: DragEvent) => { const target = (e.target as HTMLElement).closest("[data-site-id]") as HTMLElement if (!target) return // Check if element is actually draggable if (target.getAttribute("draggable") !== "true") return const siteId = target.dataset.siteId const position = parseInt(target.dataset.sitePosition || target.dataset.devicePosition || "0") if (target.classList.contains("site-header")) { this.draggedElement = target this.dragData = { type: "site", id: siteId!, siteId: siteId!, position } target.classList.add("opacity-50") } else if (target.classList.contains("device-row")) { const deviceId = target.dataset.deviceId this.draggedElement = target this.dragData = { type: "device", id: deviceId!, siteId: siteId!, position } target.classList.add("opacity-50") } e.dataTransfer!.effectAllowed = "move" } // Clean up on drag end this.handleDragEnd = (_e: DragEvent) => { if (this.draggedElement) { this.draggedElement.classList.remove("opacity-50") this.draggedElement = null } this.dragData = null // Remove drop indicators this.el.querySelectorAll(".border-blue-500").forEach((el: Element) => { el.classList.remove("border-blue-500", "border-t-4", "border-b-4") }) } // Show drop indicator this.handleDragOver = (e: DragEvent) => { e.preventDefault() if (!this.dragData) return const target = (e.target as HTMLElement).closest("[data-site-id], [data-device-id]") as HTMLElement if (!target || target === this.draggedElement) return // Check if in bottom half of element const rect = target.getBoundingClientRect() const middleY = rect.top + (rect.height / 2) const isBottomHalf = e.clientY > middleY // Visual feedback - show border on top or bottom depending on drop position this.el.querySelectorAll(".border-blue-500").forEach((el: Element) => { el.classList.remove("border-blue-500", "border-t-4", "border-b-4") }) target.classList.add("border-blue-500", isBottomHalf ? "border-b-4" : "border-t-4") } // Handle drop this.handleDrop = (e: DragEvent) => { e.preventDefault() if (!this.dragData) return const target = (e.target as HTMLElement).closest("[data-site-id], [data-device-id]") as HTMLElement if (!target || target === this.draggedElement) return const dropSiteId = target.dataset.siteId let dropPosition = parseInt(target.dataset.sitePosition || target.dataset.devicePosition || "0") // Check if dropping in bottom half of element - if so, insert after instead of before const rect = target.getBoundingClientRect() const middleY = rect.top + (rect.height / 2) if (e.clientY > middleY) { dropPosition += 1 } // Push to LiveView based on type if (this.dragData.type === "site" && target.classList.contains("site-header")) { this.pushEvent("reorder_site", { site_id: this.dragData.id, new_position: dropPosition }) } else if (this.dragData.type === "device" && target.classList.contains("device-row")) { // Only allow within same site if (dropSiteId === this.dragData.siteId) { this.pushEvent("reorder_device", { device_id: this.dragData.id, new_position: dropPosition }) } } // Clean up this.el.querySelectorAll(".border-blue-500").forEach((el: Element) => { el.classList.remove("border-blue-500", "border-t-4", "border-b-4") }) } // Add event listeners this.el.addEventListener("dragstart", this.handleDragStart) this.el.addEventListener("dragend", this.handleDragEnd) this.el.addEventListener("dragover", this.handleDragOver) this.el.addEventListener("drop", this.handleDrop) // Prevent drag handle from triggering row click this.dragHandleListeners = [] this.el.querySelectorAll(".drag-handle").forEach((handle: Element) => { const handler = (e: Event) => { e.stopPropagation() } handle.addEventListener("click", handler) this.dragHandleListeners.push({ element: handle, handler }) }) }, destroyed(this: any) { // Remove main event listeners if (this.handleDragStart) { this.el.removeEventListener("dragstart", this.handleDragStart) this.handleDragStart = null } if (this.handleDragEnd) { this.el.removeEventListener("dragend", this.handleDragEnd) this.handleDragEnd = null } if (this.handleDragOver) { this.el.removeEventListener("dragover", this.handleDragOver) this.handleDragOver = null } if (this.handleDrop) { this.el.removeEventListener("drop", this.handleDrop) this.handleDrop = null } // Remove drag handle listeners this.dragHandleListeners.forEach(({ element, handler }) => { element.removeEventListener("click", handler) }) this.dragHandleListeners = [] // Clean up state this.draggedElement = null this.dragData = null } }