// app/javascript/controllers/carrossel_controller.js // Stimulus — paginação em "carrossel" para listas longas: mostra N itens por vez // com setas grandes e o indicador "1–10 de 12". // // PADRÃO EM TELA PEQUENA, LISTA INTEIRA EM TELA GRANDE. Em celular e monitor // pequeno, uma lista de 30 cards vira rolagem infinita e o rodapé/ações da tela // somem de vista; num monitor grande a mesma lista cabe e paginar só atrapalha // (Ctrl+F do navegador não acha o que está escondido). O corte é por largura de // janela — `maxLargura` — e é reavaliado no resize/rotação da tela. // // 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 //
<- cada item // … // //
// // 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 }, maxLargura: { type: Number, default: 1366 } } connect() { this.pagina = this.paginaInicial() 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) { this.pagina = Math.min(Math.max(destino, 0), this.totalPaginas - 1) this.render() // Volta ao topo da lista: sem isso, na 2ª página o leitor continua vendo o // fim da anterior e parece que nada mudou. this.element.scrollIntoView({ behavior: "smooth", block: "start" }) } // Tela grande: sem paginação (a lista inteira aparece). get paginando() { return window.innerWidth <= this.maxLarguraValue && this.itemTargets.length > this.tamanhoValue } get totalPaginas() { return Math.max(Math.ceil(this.itemTargets.length / this.tamanhoValue), 1) } paginaInicial() { const i = this.itemTargets.findIndex((el) => el.dataset.carrosselInicial === "true") return i < 0 ? 0 : Math.floor(i / this.tamanhoValue) } 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 } // Uma tela menor pode ter reduzido o total de páginas desde a última vez. this.pagina = Math.min(this.pagina, this.totalPaginas - 1) const inicio = this.pagina * this.tamanhoValue const fim = inicio + this.tamanhoValue 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–10 de 12" e não "página 1 de 2": 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) } }