// app/javascript/controllers/carrossel_controller.js
// Stimulus — paginação em "carrossel" para listas longas: mostra uma página de
// itens por vez, com setas grandes e o indicador "1–5 de 12".
//
// TAMANHO DE PÁGINA POR TELA (é o ponto do controller):
// • celular (até 640px) → 5 itens — cada card ocupa muita altura;
// 10 já viravam rolagem infinita;
// • até 1366px → 10 itens — tablet e notebook pequeno;
// • acima disso → lista INTEIRA, sem setas. Em monitor grande a
// lista cabe e esconder item só atrapalha (o
// Ctrl+F do navegador não acha o que está oculto).
//
// O corte é reavaliado no resize/rotação: girar o celular ou encostar a janela
// na lateral já muda o comportamento, e o item que estava no topo continua na
// tela (a posição é guardada pelo ÍNDICE do item, não pelo número da página).
//
// Os itens já vêm todos no HTML; aqui só escondemos os de fora da página. Por
// isso não serve para listas de centenas de itens (aí é paginação no servidor).
//
// Uso:
//
<- acima disso, mostra tudo
//
…
// <%= render 'shared/carrossel_controles' %>
//
//
// Um item com data-carrossel-inicial="true" (ex.: o motorista com a composição
// aberta) define em qual página o carrossel abre — senão o clique dele "sumia".
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["item", "controles", "indicador", "anterior", "proximo"]
static values = {
tamanho: { type: Number, default: 10 },
tamanhoCompacto: { type: Number, default: 5 },
larguraCompacta: { type: Number, default: 640 },
maxLargura: { type: Number, default: 1366 }
}
connect() {
// Âncora = índice do primeiro item da página atual. Guardar o ÍNDICE (e não
// o número da página) é o que mantém o mesmo item na tela quando o tamanho
// da página muda junto com a largura.
this.ancora = this.indiceInicial()
this._aoRedimensionar = () => this.render()
window.addEventListener("resize", this._aoRedimensionar)
this.render()
}
disconnect() {
window.removeEventListener("resize", this._aoRedimensionar)
}
proxima() { this.ir(this.pagina + 1) }
anterior() { this.ir(this.pagina - 1) }
ir(destino) {
const pagina = Math.min(Math.max(destino, 0), this.totalPaginas - 1)
this.ancora = pagina * this.tamanho
this.render()
// Volta ao topo da lista: sem isso, na página seguinte o leitor continua
// vendo o fim da anterior e parece que nada mudou.
this.element.scrollIntoView({ behavior: "smooth", block: "start" })
}
get tamanho() {
return window.innerWidth <= this.larguraCompactaValue
? this.tamanhoCompactoValue
: this.tamanhoValue
}
// Tela grande: sem paginação (a lista inteira aparece).
get paginando() {
return window.innerWidth <= this.maxLarguraValue &&
this.itemTargets.length > this.tamanho
}
get totalPaginas() {
return Math.max(Math.ceil(this.itemTargets.length / this.tamanho), 1)
}
get pagina() {
return Math.min(Math.floor(this.ancora / this.tamanho), this.totalPaginas - 1)
}
indiceInicial() {
const i = this.itemTargets.findIndex((el) => el.dataset.carrosselInicial === "true")
return i < 0 ? 0 : i
}
render() {
const total = this.itemTargets.length
if (!this.paginando) {
this.itemTargets.forEach((item) => item.classList.remove("hidden"))
if (this.hasControlesTarget) this.controlesTarget.classList.add("hidden")
return
}
const inicio = this.pagina * this.tamanho
const fim = inicio + this.tamanho
this.itemTargets.forEach((item, i) => {
item.classList.toggle("hidden", i < inicio || i >= fim)
})
if (!this.hasControlesTarget) return
this.controlesTarget.classList.remove("hidden")
if (this.hasIndicadorTarget) {
// "1–5 de 12" e não "página 1 de 3": quem lê quer saber quantos itens
// existem, não quantas páginas a tela tem.
this.indicadorTarget.textContent = `${inicio + 1}–${Math.min(fim, total)} de ${total}`
}
if (this.hasAnteriorTarget) this.alternar(this.anteriorTarget, this.pagina === 0)
if (this.hasProximoTarget) this.alternar(this.proximoTarget, this.pagina >= this.totalPaginas - 1)
}
// Botão de ponta: desabilitado de verdade (não só apagado), para o toque não
// "funcionar sem fazer nada".
alternar(botao, desligado) {
botao.disabled = desligado
botao.classList.toggle("opacity-30", desligado)
botao.classList.toggle("pointer-events-none", desligado)
}
}