Files
Reem-Notas/app/javascript/controllers/carrossel_controller.js

134 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 "15 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:
// <div data-controller="carrossel"
// data-carrossel-tamanho-value="10" <- itens por página
// data-carrossel-tamanho-compacto-value="5" <- itens no celular
// data-carrossel-largura-compacta-value="640" <- até aqui é "celular"
// data-carrossel-max-largura-value="1366"> <- acima disso, mostra tudo
// <div data-carrossel-target="item">…</div>
// <%= render 'shared/carrossel_controles' %>
// </div>
//
// 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"))
this.controlesTargets.forEach((el) => el.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.controlesTargets.forEach((el) => el.classList.remove("hidden"))
// "15 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.
const rotulo = `${inicio + 1}${Math.min(fim, total)} de ${total}`
this.indicadorTargets.forEach((el) => { el.textContent = rotulo })
// Plural: o mesmo controle aparece em dois layouts (setas laterais em tela
// com espaço, barra flutuante no rodapé no celular) e os dois precisam
// desabilitar juntos.
this.anteriorTargets.forEach((b) => this.alternar(b, this.pagina === 0))
this.proximoTargets.forEach((b) => this.alternar(b, 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)
}
}