168 lines
5.8 KiB
TypeScript
168 lines
5.8 KiB
TypeScript
/**
|
|
* 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
|
|
}
|
|
}
|