151 lines
4.6 KiB
JavaScript
151 lines
4.6 KiB
JavaScript
import { Controller } from "@hotwired/stimulus"
|
|
import { post } from "@rails/request.js"
|
|
import { nextFrame } from "helpers/timing_helpers"
|
|
|
|
export default class extends Controller {
|
|
static targets = [ "item", "container" ]
|
|
static classes = [ "draggedItem", "hoverContainer" ]
|
|
|
|
// Actions
|
|
|
|
async dragStart(event) {
|
|
event.dataTransfer.effectAllowed = "move"
|
|
event.dataTransfer.dropEffect = "move"
|
|
event.dataTransfer.setData("37ui/move", event.target)
|
|
|
|
await nextFrame()
|
|
this.dragItem = this.#itemContaining(event.target)
|
|
this.sourceContainer = this.#containerContaining(this.dragItem)
|
|
this.originalDraggedItemCssVariable = this.#containerCssVariableFor(this.sourceContainer)
|
|
this.dragItem.classList.add(this.draggedItemClass)
|
|
}
|
|
|
|
dragOver(event) {
|
|
event.preventDefault()
|
|
if (!this.dragItem) { return }
|
|
|
|
const container = this.#containerContaining(event.target)
|
|
this.#clearContainerHoverClasses()
|
|
|
|
if (!container) { return }
|
|
|
|
if (container !== this.sourceContainer) {
|
|
container.classList.add(this.hoverContainerClass)
|
|
this.#applyContainerCssVariable(container)
|
|
} else {
|
|
this.#restoreOriginalDraggedItemCssVariable()
|
|
}
|
|
}
|
|
|
|
async drop(event) {
|
|
const targetContainer = this.#containerContaining(event.target)
|
|
|
|
if (!targetContainer || targetContainer === this.sourceContainer) { return }
|
|
|
|
this.wasDropped = true
|
|
this.#increaseCounter(targetContainer)
|
|
this.#decreaseCounter(this.sourceContainer)
|
|
|
|
const sourceContainer = this.sourceContainer
|
|
this.#insertDraggedItem(targetContainer, this.dragItem)
|
|
await this.#submitDropRequest(this.dragItem, targetContainer)
|
|
this.#reloadSourceFrame(sourceContainer)
|
|
}
|
|
|
|
dragEnd() {
|
|
this.dragItem.classList.remove(this.draggedItemClass)
|
|
this.#clearContainerHoverClasses()
|
|
|
|
if (!this.wasDropped) {
|
|
this.#restoreOriginalDraggedItemCssVariable()
|
|
}
|
|
|
|
this.sourceContainer = null
|
|
this.dragItem = null
|
|
this.wasDropped = false
|
|
this.originalDraggedItemCssVariable = null
|
|
}
|
|
|
|
#itemContaining(element) {
|
|
return this.itemTargets.find(item => item.contains(element) || item === element)
|
|
}
|
|
|
|
#containerContaining(element) {
|
|
return this.containerTargets.find(container => container.contains(element) || container === element)
|
|
}
|
|
|
|
#clearContainerHoverClasses() {
|
|
this.containerTargets.forEach(container => container.classList.remove(this.hoverContainerClass))
|
|
}
|
|
|
|
#applyContainerCssVariable(container) {
|
|
const cssVariable = this.#containerCssVariableFor(container)
|
|
if (cssVariable) {
|
|
this.dragItem.style.setProperty(cssVariable.name, cssVariable.value)
|
|
}
|
|
}
|
|
|
|
#restoreOriginalDraggedItemCssVariable() {
|
|
if (this.originalDraggedItemCssVariable) {
|
|
const { name, value } = this.originalDraggedItemCssVariable
|
|
this.dragItem.style.setProperty(name, value)
|
|
}
|
|
}
|
|
|
|
#containerCssVariableFor(container) {
|
|
const { dragAndDropCssVariableName, dragAndDropCssVariableValue } = container.dataset
|
|
if (dragAndDropCssVariableName && dragAndDropCssVariableValue) {
|
|
return { name: dragAndDropCssVariableName, value: dragAndDropCssVariableValue }
|
|
}
|
|
return null
|
|
}
|
|
|
|
#increaseCounter(container) {
|
|
this.#modifyCounter(container, count => count + 1)
|
|
}
|
|
|
|
#decreaseCounter(container) {
|
|
this.#modifyCounter(container, count => Math.max(0, count - 1))
|
|
}
|
|
|
|
#modifyCounter(container, fn) {
|
|
const counterElement = container.querySelector("[data-drag-and-drop-counter]")
|
|
if (counterElement) {
|
|
const currentValue = counterElement.textContent.trim()
|
|
|
|
if (!/^\d+$/.test(currentValue)) return
|
|
|
|
counterElement.textContent = fn(parseInt(currentValue))
|
|
}
|
|
}
|
|
|
|
#insertDraggedItem(container, item) {
|
|
const itemContainer = container.querySelector("[data-drag-drop-item-container]")
|
|
const topItems = itemContainer.querySelectorAll("[data-drag-and-drop-top]")
|
|
const firstTopItem = topItems[0]
|
|
const lastTopItem = topItems[topItems.length - 1]
|
|
|
|
const isTopItem = item.hasAttribute("data-drag-and-drop-top")
|
|
const referenceItem = isTopItem ? firstTopItem : lastTopItem
|
|
|
|
if (referenceItem) {
|
|
referenceItem[isTopItem ? "before" : "after"](item)
|
|
} else {
|
|
itemContainer.prepend(item)
|
|
}
|
|
}
|
|
|
|
async #submitDropRequest(item, container) {
|
|
const body = new FormData()
|
|
const id = item.dataset.id
|
|
const url = container.dataset.dragAndDropUrl.replaceAll("__id__", id)
|
|
|
|
return post(url, { body, headers: { Accept: "text/vnd.turbo-stream.html" } })
|
|
}
|
|
|
|
#reloadSourceFrame(sourceContainer) {
|
|
const frame = sourceContainer.querySelector("[data-drag-and-drop-refresh]")
|
|
if (frame) frame.reload()
|
|
}
|
|
}
|