// 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) } }