191 lines
4.8 KiB
JavaScript
191 lines
4.8 KiB
JavaScript
|
import 'core-js/stable'
|
||
|
import 'regenerator-runtime/runtime'
|
||
|
import { Controller } from 'stimulus'
|
||
|
|
||
|
/*
|
||
|
* Permite reordenar las filas de una tabla.
|
||
|
*
|
||
|
* Cada fila tiene un selector que permite decidir si la fila se
|
||
|
* mantiene en su lugar o se mueve al presionar las teclas de subir y
|
||
|
* bajar.
|
||
|
*
|
||
|
* Se pueden mover varias juntas.
|
||
|
*
|
||
|
* El controlador está en la tabla y cada fila es un objetivo. Dentro
|
||
|
* de cada fila tiene que haber un input[type=checkbox] que determina si
|
||
|
* está seleccionada o no y un input[type=hidden] que contiene la
|
||
|
* posición actual que luego será guardada.
|
||
|
*
|
||
|
* La tabla tiene que estar rodeada de un formulario para poder enviar
|
||
|
* los datos.
|
||
|
*
|
||
|
* El objetivo es poder mover filas en tablas de miles de elementos.
|
||
|
*/
|
||
|
export default class extends Controller {
|
||
|
static targets = [ 'row', 'unselect', 'top', 'bottom', 'direction' ]
|
||
|
|
||
|
connect () {
|
||
|
// Deseleccionar
|
||
|
this.unselectTarget.addEventListener('click', event => {
|
||
|
event.preventDefault()
|
||
|
event.stopPropagation()
|
||
|
|
||
|
for (const r of Object.values(this.selected_rows)) {
|
||
|
r.row.querySelector('[data-reorder-handler]').click()
|
||
|
}
|
||
|
})
|
||
|
|
||
|
// Enviar arriba de todo
|
||
|
this.topTarget.addEventListener('click', event => {
|
||
|
event.preventDefault()
|
||
|
event.stopPropagation()
|
||
|
|
||
|
if (this.empty) return
|
||
|
|
||
|
const rows = this.sorted_rows()
|
||
|
const first = rows[0].row.parentElement.firstElementChild
|
||
|
|
||
|
for (const r of rows) {
|
||
|
const row = r.row
|
||
|
|
||
|
if (row === first) continue
|
||
|
|
||
|
row.parentElement.insertBefore(row, first)
|
||
|
}
|
||
|
|
||
|
// Reacomodamos el orden
|
||
|
this.reorder()
|
||
|
|
||
|
// Mantenemos el primero a la vista
|
||
|
rows[0].row.scrollIntoViewIfNeeded()
|
||
|
})
|
||
|
|
||
|
// Enviar al final
|
||
|
this.bottomTarget.addEventListener('click', event => {
|
||
|
event.preventDefault()
|
||
|
event.stopPropagation()
|
||
|
|
||
|
if (this.empty) return
|
||
|
|
||
|
const rows = this.sorted_rows()
|
||
|
|
||
|
for (const r of rows) {
|
||
|
const row = r.row
|
||
|
|
||
|
row.parentElement.appendChild(row)
|
||
|
}
|
||
|
|
||
|
// Reacomodamos el orden
|
||
|
this.reorder()
|
||
|
|
||
|
// Mantenemos el primero a la vista
|
||
|
rows[0].row.scrollIntoViewIfNeeded()
|
||
|
})
|
||
|
|
||
|
this.rowTargets.forEach(row => {
|
||
|
// Al cambiar los inputs, mantener la lista de filas actualizadas.
|
||
|
// Necesitamos saber la posición para poder mover las filas en
|
||
|
// orden en lugar del orden en que fueron seleccionadas.
|
||
|
row.querySelector('[data-reorder-handler]').addEventListener('change', event => {
|
||
|
if (event.target.checked) {
|
||
|
this.selected_rows[row.id] = {
|
||
|
row,
|
||
|
order: this.rowTargets.indexOf(row)
|
||
|
}
|
||
|
} else {
|
||
|
delete this.selected_rows[row.id]
|
||
|
}
|
||
|
})
|
||
|
})
|
||
|
|
||
|
this.directionTargets.forEach(dir => {
|
||
|
dir.addEventListener('click', event => {
|
||
|
event.preventDefault()
|
||
|
event.stopPropagation()
|
||
|
if (this.empty) return
|
||
|
|
||
|
this.move(dir.dataset.direction)
|
||
|
})
|
||
|
})
|
||
|
|
||
|
// Lo asociamos al documento porque en la tabla se pierde el foco
|
||
|
// luego del primer evento.
|
||
|
document.addEventListener('keydown', event => {
|
||
|
if (!this.codes.includes(event.keyCode)) return
|
||
|
if (this.empty) return
|
||
|
|
||
|
event.preventDefault()
|
||
|
|
||
|
this.move(event.keyCode === 38 ? 'up' : 'down')
|
||
|
})
|
||
|
}
|
||
|
|
||
|
get selected_rows () {
|
||
|
if (!this._selected_rows) this._selected_rows = {}
|
||
|
|
||
|
return this._selected_rows
|
||
|
}
|
||
|
|
||
|
// Arriba, abajo
|
||
|
get codes () {
|
||
|
if (!this._codes) this._codes = [ 38, 40 ]
|
||
|
|
||
|
return this._codes
|
||
|
}
|
||
|
|
||
|
get empty () {
|
||
|
return (Object.keys(this.selected_rows).length === 0)
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Aplica el nuevo orden en las filas y sus campos
|
||
|
*/
|
||
|
reorder () {
|
||
|
for (const r of Object.values(this.selected_rows)) {
|
||
|
this.selected_rows[r.row.id].order = this.rowTargets.indexOf(r.row)
|
||
|
}
|
||
|
|
||
|
const length = this.rowTargets.length
|
||
|
|
||
|
this.rowTargets.forEach((row, i) => {
|
||
|
row.querySelector('[data-reorder]').value = length - i
|
||
|
})
|
||
|
}
|
||
|
|
||
|
sorted_rows () {
|
||
|
return Object.values(this.selected_rows).sort((a,b) => a.order - b.order)
|
||
|
}
|
||
|
|
||
|
move (direction) {
|
||
|
if (this.empty) return
|
||
|
|
||
|
const up = direction === 'up'
|
||
|
const down = !up
|
||
|
const direction_sibling = up ? 'previousElementSibling' : 'nextElementSibling'
|
||
|
|
||
|
// Los movemos en orden
|
||
|
const rows = this.sorted_rows()
|
||
|
if (down) rows.reverse()
|
||
|
|
||
|
for (const r of rows) {
|
||
|
const row = r.row
|
||
|
const sibling = row[direction_sibling]
|
||
|
|
||
|
// Estamos en el tope?
|
||
|
if (!sibling || sibling.tagName !== row.tagName) continue
|
||
|
|
||
|
if (up) {
|
||
|
row.parentElement.insertBefore(row, sibling)
|
||
|
} else {
|
||
|
row.parentElement.insertBefore(sibling, row)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Reacomodamos el orden
|
||
|
this.reorder()
|
||
|
|
||
|
// Mantenemos el primero a la vista
|
||
|
rows[0].row.scrollIntoViewIfNeeded()
|
||
|
}
|
||
|
}
|