import { Controller } from '@hotwired/stimulus' import Sortable from 'sortablejs' export default class extends Controller { static targets = [ 'list', 'item', 'handle', 'positionField', 'positionLabel', 'status' ] static values = { url: String, } connect() { this.abortController = null // The sortable container may be nested (e.g. a tbody) when the controller element also // has to wrap elements that must not be dragged, like the status message. new Sortable(this.hasListTarget ? this.listTarget : this.element, { draggable: '[data-sortable-target="item"]', handle: '[data-sortable-target="handle"]', sort: true, fallbackOnBody: true, onEnd: e => { this.updateOrdering() this.persist() } }) } disconnect() { this.abortController?.abort() } updateOrdering() { this.positionFieldTargets.forEach((field, index) => { field.value = index + 1 }) this.positionLabelTargets.forEach((label, index) => { label.innerText = index + 1 }) } /** * Sends the new order to the server. Only active when a url value is present, so the * controller stays usable for plain form ordering. */ persist() { if (!this.hasUrlValue || this.urlValue === '') { return } // A drag while the previous request is still running makes that request obsolete. this.abortController?.abort() this.abortController = new AbortController() const ids = this.itemTargets .map(item => Number(item.dataset.sortableId)) .filter(id => Number.isInteger(id) && id > 0) this.setStatus('speichert …') fetch(this.urlValue, { method: 'POST', credentials: 'include', signal: this.abortController.signal, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ids }), }) .then(response => { if (!response.ok) { throw new Error(`Unexpected response ${response.status}`) } this.setStatus('Reihenfolge gespeichert') }) .catch(error => { if (error.name === 'AbortError') { return } console.error('Failed to persist ordering', error) this.setStatus('Speichern fehlgeschlagen') }) } setStatus(message) { this.statusTargets.forEach(status => { status.innerText = message }) } }