O Discourse permite que os usuários façam upload de imagens em posts (e em outros lugares). Às vezes, essas imagens são grandes demais para serem exibidas diretamente, então o Discourse cria uma versão dimensionada da imagem e a adiciona ao post. Quando você clica nessa imagem dimensionada, aparece um overlay elegante contendo a imagem em tamanho original — isso é comumente chamado de “lightboxing”.
Atualmente, o Discourse utiliza uma biblioteca chamada Magnific Popup para gerenciar o comportamento de lightbox. Este tópico trata de uma reformulação dos lightboxes no Discourse, migrando do Magnific Popup para uma solução mais alinhada com as expectativas atuais de usuários e desenvolvedores.
O porquê
O Magnific é uma biblioteca excelente, e o número de estrelas na página do projeto indica significativamente quantos problemas ela resolveu para usuários e desenvolvedores ao longo dos anos.
Ela também estava à frente de seu tempo em termos de facilidade de uso e recursos. Isso fica evidente ao considerar que quase toda a funcionalidade que você vê hoje já estava presente na versão 1.0, lançada em 2014.
Então, onde isso nos deixa? Vou ser breve. O Magnific Popup foi criado para um mundo completamente diferente, onde a compatibilidade entre navegadores estava muito atrás do que é hoje. Isso significa quatro coisas:
- Ele depende do jQuery.
- Contém código destinado a navegadores antigos.
- O dispositivo médio usado para acessar a web mudou muito desde então.
- Foi projetado com algo diferente de aplicações de página única (como o Discourse) como prioridade — especificamente, páginas estáticas. Isso gera preocupações de desempenho específicas para aplicações de página única, que abordaremos mais adiante.
Dado o estado atual dos padrões da web e como é possível fazer todo tipo de mágica com JavaScript puro (vanilla), é compreensível que grandes projetos como o Discourse estejam se afastando da dependência do jQuery. Note que discussões sobre o jQuery estão fora do escopo aqui; isso serve apenas como contexto.
Então, esse é o porquê… menos jQuery, menos código para navegadores antigos, melhor desempenho geral e melhor suporte para o dispositivo médio — agora muito diferente.
O como
Não há escassez de soluções disponíveis, e há uma discussão a ser feita sobre não reinventar a roda, mas… vamos evitar esse caminho e manter as coisas breves.
Talvez a maneira mais curta de abordar esse ponto difícil seja dizer… não importa o quão bem algo pronto se encaixe… nada se encaixará tão bem quanto algo feito sob medida. Vamos ficar por aqui.
Há várias coisas a considerar aqui, seja excesso de funcionalidade em bibliotecas que cobrem numerosos casos de uso, como imagens, iframes, vídeos e afins, ou falta de clareza em torno das licenças.
Conhecer os casos de uso específicos que o Discourse precisa suportar torna possível evitar código desnecessário, permitindo que recursos adicionais e direcionados sejam adicionados sem gerar muita sobrecarga.
Portanto, agora temos uma boa base. Os requisitos são:
- Sem jQuery.
- Focar apenas em imagens por enquanto.
- Suporte a melhorias de qualidade de vida, como deslizar (swipe) em dispositivos móveis.
- Integração com o Discourse para permitir futuras personalizações e melhorias usando os sistemas atuais de temas e plugins.
O quê
O Discourse é um projeto baseado em Ember, então o novo lightbox precisaria ser um componente Ember para lidar com o markup e os dados. Além disso, como há a expectativa de que seja possível configurar lightboxes em qualquer lugar do aplicativo, ter um serviço de lightbox faz muito sentido. Isso permitirá que os desenvolvedores:
- Injetem o serviço em qualquer componente e possam vincular os métodos de configuração/limpeza do serviço ao ciclo de vida do componente.
- Procurem o serviço em qualquer lugar do aplicativo e possam chamar seus métodos de configuração/limpeza.
Além disso, como uma melhoria de qualidade de vida, podemos adicionar um pouco de abstração e criar algumas funções utilitárias que podem ser usadas em temas. Mais sobre isso depois.
Como um dos objetivos é permitir que temas e plugins estendam a funcionalidade, o serviço de lightbox comunicará mudanças de estado por meio de appEvents. Ele emitirá os seguintes eventos:
lightbox:openedlightbox:item-will-changelightbox:item-did-changelightbox:closed
Cada evento será acionado com o tipo de dados que os desenvolvedores normalmente esperariam. Por exemplo, o evento lightbox:opened conterá uma lista de todos os itens e o item atual em que o lightbox foi aberto. Mais sobre isso depois.
Com isso resolvido, vamos continuar.
Nas últimas semanas, tenho trabalhado em um PR de introdução do Discourse Lightbox.
À primeira vista, pode parecer um pouco grande, então vamos decompor.
Testar algo que depende de muita interação do usuário é complicado, por isso o conjunto de testes do lightbox inclui 63 testes com 291 asserções.
Com o tamanho dos testes fora do caminho, vamos fazer uma rápida comparação de tamanho com o Magnific Popup (ambos não minificados):
| Discourse Lightbox LOC | Magnific Popup LOC | Diferença | |
|---|---|---|---|
| JavaScript | 1197 | 1860 | (35%) |
| CSS | 813 | 351 | 131% |
| Templates | 401 | 0 | - |
| Total | 2411 | 2211 | 9.1% |
Portanto, o Discourse Lightbox tem cerca de 9% a mais de código que o Magnific. Claro, isso é apenas parte da história por dois motivos:
- Não consideramos o jQuery, do qual o Magnific depende. O jQuery 3.6 tem aproximadamente 11k LOC.
- O Discourse Lightbox adiciona mais recursos em comparação ao Magnific.
Vamos entrar nesses recursos.
Qualquer travamento que você veja nos vídeos abaixo está relacionado à gravação, não à experiência que os usuários terão. Todas as animações/transições serão executadas a 60 FPS sólidos.
Então, sem mais delongas,
Layout básico
Para comparação, aqui está o mesmo na implementação atual do Magnific:
Alguns pontos sobre os vídeos acima:
-
O novo lightbox usará a imagem como fundo (backdrop) em vez do fundo escuro semitransparente genérico. A imagem, por definição, se complementará. Note que isso não adiciona nenhuma sobrecarga de rede. A imagem usada no fundo é a do corpo do post, o que significa que ela já estará em cache quando os usuários abrirem o lightbox.
-
O Discourse Lightbox opta por uma interface fixa. Em vez de ter o botão de fechar, o título e os metadados da imagem anexados à imagem, eles têm seus próprios espaços reservados. Isso ajudará a reduzir oscilações ao navegar entre imagens de dimensões diferentes.
-
Navegar entre imagens pode ser feito com as setas no lightbox ou por meio de atalhos de teclado. → ou ↓ para o próximo e ← ou ↑ para o anterior. Em locais com direção da direita para a esquerda (RTL), os atalhos são invertidos conforme necessário.
O layout em dispositivos móveis é muito semelhante, exceto que as setas não são exibidas, pois há gestos de deslizar (swipe) para navegação.
Deslize para a esquerda no dispositivo móvel para o próximo e para a direita para o anterior. Em locais RTL, os gestos de deslizar são invertidos.
Zoom
O novo lightbox possui um botão de zoom dedicado, visível quando a imagem que você está visualizando é maior que as dimensões da viewport. Clicar no botão de zoom aumentará a imagem, e clicar novamente diminuirá. Além disso, agora há um atalho de teclado para zoom in/out: Z. Por fim, se a imagem for ampliável, clicar nela terá o mesmo efeito.
Aqui está um vídeo demonstrando os três métodos:
Note que, embora não seja demonstrado no vídeo acima, o cursor, ao passar o mouse sobre uma imagem que pode ser ampliada, mudará para refletir isso. Ele também mudará para um ícone de zoom-out ao passar o mouse sobre uma imagem que já está ampliada.
O zoom funciona de maneira diferente no novo lightbox. No desktop, a seção ampliada da imagem seguirá o cursor.
Não há hover em dispositivos móveis; usa o toque normal (touch-scroll) para se mover dentro de uma imagem ampliada.
Rotação
O novo lightbox adiciona um botão de rotação dedicado para girar a imagem em incrementos de 90 graus. A rotação também possui um atalho de teclado: a tecla R. Veja como isso fica:
Rotação e zoom podem ser combinados.
Modo de tela cheia
O novo lightbox adiciona um botão de tela cheia. O botão fará com que a janela do navegador entre no modo de tela cheia. O atalho de teclado é M.
Ele manterá o rastreamento de seu estado e retornará ao modo normal quando a tela cheia for desativada ou quando o lightbox for fechado.
Download
O lightbox atual adiciona um link de “download” abaixo da imagem. O novo lightbox faz o mesmo, mas altera para um ícone e o adiciona ao rodapé do lightbox. Ele ainda respeita as mesmas permissões. Se…
prevent_anons_from_downloading_images
estiver ativado e o usuário não estiver logado, o ícone de download não será exibido.
Nova Aba
O lightbox atual adiciona um link de “original” abaixo da imagem que a abre em uma nova aba. O novo lightbox faz o mesmo, mas adiciona como um ícone no cabeçalho do lightbox. Ele também respeitará a mesma permissão definida para o ícone de download.
Título da Imagem
O novo lightbox foca principalmente na imagem. Os títulos das imagens são truncados para uma linha por padrão, mas suportam expansão. Aqui está um exemplo de como isso fica:
Há um atalho de teclado para expandir/recolher o título: T, e o título não será exibido quando a imagem estiver ampliada ou rotacionada.
Carrossel
Um recurso recém-disponível é mostrar todas as imagens da galeria em um carrossel. O layout dependerá da tela do dispositivo; pode ser horizontal ou vertical, e aqui está como fica:
Há um atalho de teclado para alternar o carrossel: A.
E aqui está como fica em dispositivos móveis:
Deslizar para baixo em dispositivos móveis alternará o carrossel ligado/desligado.
Fechamento
O Esc ainda funciona como antes no desktop para fechar o lightbox. Agora há um gesto de deslizar adicional em dispositivos móveis: você pode deslizar para cima para fechar o lightbox.
Acessibilidade
Além do básico, como rótulos de botões, o novo lightbox adiciona um elemento anunciador para leitores de tela fora da tela. Ao navegar para uma imagem dentro do lightbox, ele lerá seu índice e título com base no seguinte formato:
imagem %{current} de %{total}: %{title}
Aqui está um breve exemplo disso:
O novo lightbox também remove todos os botões desnecessários que não servem a nenhum propósito para leitores de tela, usando aria-hidden.
Lembre-se de que a acessibilidade é uma missão contínua e isso está longe de estar completo. Sempre há algo a melhorar, mas parei aqui para manter as coisas simples para a versão 1.
Isso cobre todos os recursos do novo lightbox.
Vamos passar para um pouco de linguagem de desenvolvedor.
Listeners de Eventos
O lightbox atual adiciona listeners de evento de clique a cada imagem individual de lightbox em posts processados. Isso significa que um post com 20 imagens terá 20 listeners de evento de clique para lightboxes.
O novo lightbox aproveita a delegação de eventos e adiciona apenas um listener de evento ao próprio post.
Aqui está a contagem de listeners de evento do lightbox atual em uma janela anônima/incógnita em um post com 20 imagens após forçar a coleta de lixo:
E aqui está o mesmo post com o novo lightbox:
Além disso, navegar dentro do lightbox atualmente adiciona listeners de evento, que acabam ficando órfãos e, portanto, interferem na coleta de lixo. Aqui está um gráfico de:
- Carregar uma página de tópico com um post contendo 20 imagens.
- Abrir o lightbox e navegar por todas as 20 imagens três vezes consecutivas.
- Fechar o lightbox.
- Forçar a coleta de lixo do navegador.
Lightbox atual:
Novo lightbox:
Quanto aos listeners de evento nos próprios lightboxes, eles parecem não ser limpos atualmente no Magnific (lembre-se, ele não foi construído para aplicações de página única).
Uma nota rápida sobre os testes acima: eles são muito rudimentares e os números não devem ser considerados “científicos”; o objetivo aqui é descobrir a direção, não os números exatos.
Notas para Desenvolvedores
Vamos falar sobre configurar e limpar lightboxes com o novo Discourse Lightbox.
Configurando e limpando lightboxes
Desenvolvedores têm duas opções.
-
Injetar o serviço de lightbox em um componente via:
import { inject as service } from "@ember/service"; //... @service lightboxEm seguida, você pode chamar:
this.lightbox.setupLightboxes({ container: yourContainer // Nó DOM selector: ".css-selector" // Seletor de string para os elementos que você deseja lightbox })Sempre que quiser limpar, basta chamar:
this.lightbox.cleanupLightboxes()É isso.
-
Se você não quiser injetar o serviço de lightbox, pode importar
setupLightboxesecleanupLightboxesassim:import { cleanupLightboxes, setupLightboxes, } from "discourse/lib/lightbox";O resto é o mesmo que injetar o serviço. Essas duas funções procurarão o serviço por você. Então:
setupLightboxes({ container: yourContainer // Nó DOM selector: ".css-selector" // Seletor de string para os elementos que você deseja lightbox }) //.... cleanupLightboxes()
Note que tanto chamar diretamente do serviço quanto via funções utilitárias também aceitarão um nodeList para compatibilidade retroativa, mas não é recomendado.
Uma última nota sobre isso: você também pode fazer com que um elemento não imagem atue como um gatilho para abrir um lightbox que você configurou. Por exemplo:
<div class="my-container">
<img class="my-selector" src="foo">
<img class="my-selector" src="bar">
....
<button>Abrir Lightbox</button>
</div>
Eu faria algo assim para configurar lightboxes básicos no div acima:
import {
cleanupLightboxes,
setupLightboxes,
} from "discourse/lib/lightbox";
//...
setupLightboxes({
container: document.querySelector(".my-container"),
selector: ".my-selector"
})
Para fazer o botão abrir o lightbox, tudo o que você precisa fazer é adicionar data-lightbox-trigger assim:
<button data-lightbox-trigger>Abrir Lightbox</button>
O resto é tratado automaticamente.
Finalmente, sempre que quiser limpar, chame:
cleanupLightboxes()
A limpeza não é realmente crítica, pois o serviço de lightbox limpará automaticamente sempre que o evento dom:clean for disparado no aplicativo (em transições de rota).
Ouvindo eventos do lightbox
O novo lightbox disparará eventos, como discutimos anteriormente. Esses eventos são:
lightbox:openedlightbox:item-will-changelightbox:item-did-changelightbox:closed
lightbox:opened
Este evento será disparado quando o lightbox for aberto e conterá dois objetos.
items: este é um array de todas as imagens no lightbox atual. Cada uma delas será um objeto.currentItem: este é o objeto do item atual em que o lightbox foi aberto.
Um objeto de item se parece com isso:
{
"fullsizeURL": "https://global.discourse-cdn.com/meta/original/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff.jpeg",
"smallURL": "https://global.discourse-cdn.com/meta/optimized/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff_2_600x750.jpeg",
"downloadURL": "/uploads/short-url/56rKTvkvmL6c2C8OFQtMo3D8sIn.jpeg?dl=1",
"title": "Close-up Photo of a Black Microphone on Stand",
"fileDetails": "1200×1500 88.9 KB",
"dominantColor": "793C6D",
"aspectRatio": "400 / 500",
"index": 0,
"cssVars": "--dominant-color: #793C6D;--aspect-ratio: 400 / 500;--small-url: url(https://global.discourse-cdn.com/meta/optimized/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff_2_600x750.jpeg);",
"isLoaded": true,
"hasLoadingError": false,
"width": 1200,
"height": 1500,
"canZoom": true
}
lightbox:item-will-change
Este evento é disparado logo antes do item atual no lightbox mudar. Ele terá o currentItem (o que está prestes a mudar).
lightbox:item-did-change
Este evento é disparado logo após o item no lightbox mudar e terminar de carregar, e terá currentItem como argumento.
lightbox:closed
Este evento é disparado logo após o lightbox ser fechado e não tem argumentos.
Com os eventos acima, um componente de tema teórico pode facilmente adicionar análises ao lightbox assim:
api.onAppEvent('lightbox:opened', ({items, currentItem}) => {
console.log({items});
console.log({currentItem});
// seu código de análise aqui
});
ou outras ideias semelhantes.
O CSS
O novo lightbox usa a convenção de nomenclatura BEM para classes HTML. Aqui está uma lista completa dos seletores que você pode usar:
html.has-lightbox {
// css para o elemento HTML quando os lightboxes estão abertos
}
.d-lightbox {
&--is-visible {
// Elemento principal do lightbox
}
&__content {
// Wrapper do conteúdo interno do lightbox
}
}
.d-lightbox {
&__content__header {
// Cabeçalho do lightbox
}
}
.d-lightbox {
&__content__body {
// Corpo do lightbox (contém a imagem principal)
&__backdrop {
// Fundo do lightbox
}
&__main-image {
// Imagem principal do lightbox
}
&__error-message {
// Mensagem de erro do lightbox
}
&__previous-button,
&__next-button {
// Botões anterior/próximo principais do lightbox
}
}
}
.d-lightbox {
&__content__footer {
// Rodapé do lightbox
&__main-title {
// Título da imagem do lightbox
&__item-file-details {
// Detalhes do arquivo do lightbox, ex: "1000x582 183KB"
}
}
}
}
.d-lightbox {
&__content__carousel {
// Container do carrossel do lightbox
&__previous-button,
&__next-button {
// Botões anterior/próximo do carrossel do lightbox
}
}
}
.d-lightbox {
&__content__carousel {
&__carousel-items {
// Container dos itens do carrossel do lightbox
&__item,
&__item--is-current {
// Item do carrossel do lightbox
}
&__item--is-current {
// Item atual do carrossel do lightbox
}
}
}
}
.d-lightbox {
&--is-vertical &__content__carousel {
// Estilos verticais do carrossel do lightbox
}
}
.d-lightbox {
&--is-horizontal &__content__carousel {
// Estilos horizontais do carrossel do lightbox
}
}
.d-lightbox {
.btn-flat {
// Estilos para todos os botões do lightbox
}
}
.d-lightbox {
&__content {
&__focus-trap,
&__screen-reader-announcer {
// Armadilha de foco e anunciador para leitores de tela do lightbox. Estes estão fora da tela
}
}
}
/* Estilos de estado */
// Carrossel
.d-lightbox {
&--has-carousel {
// Estilos do lightbox quando o carrossel está aberto
}
}
// Título expandido
.d-lightbox {
&--has-expanded-title {
// Estilos do lightbox quando o título está expandido
}
}
// Zoom
.d-lightbox {
&--can-zoom {
// Estilos do lightbox quando a imagem pode ser ampliada
}
&--is-zoomed {
// Estilos do lightbox quando a imagem está ampliada
}
}
// Rotação
.d-lightbox {
&--is-rotated {
// Estilos do lightbox quando a imagem está rotacionada
}
}
// Tela cheia
.d-lightbox {
&--is-fullscreen {
// Estilos do lightbox quando a imagem está em tela cheia
}
}
Agora que cobrimos o quê, finalmente podemos passar para…
O quando
No momento, o PR está pronto para revisão, o que precisa acontecer primeiro. Após passar pela revisão, estará disponível para sites que atualizarem. O PR adiciona uma nova configuração de site temporária enquanto fazemos a transição do Magnific Popup para o Discourse Lightbox. O nome da configuração é:
enable_experimental_lightbox
Se a configuração estiver desativada, o PR não terá efeito e tudo continuará funcionando como antes com o Magnific Popup.
O Discourse Lightbox substituirá o Magnific Popup em posts processados, mensagens de chat e componentes de uploader de imagem quando a configuração for ativada.
Roadmap
- Revisão do PR
- Mesclagem do PR
- Janela de feedback geral (1-2 semanas)
- PR para remover o Magnific Popup do núcleo e remover a configuração experimental do site.
- A definir: metas estendidas, como um componente de tema que explora diferentes layouts (deve ser direto, já que o novo lightbox usa CSS Grid para o layout).
Agradecimentos
- Este trabalho foi generosamente patrocinado pela CDCK

- Muito amor
vai para Dmytro Semenov, o criador do Magnific Popup, por criar algo que estava muito à frente de seu tempo. - As imagens usadas nas demonstrações acima são cortesia de Irina Iriser @pexels




