Discourse permette agli utenti di caricare immagini nei post (e in altri luoghi). A volte queste immagini sono troppo grandi per essere visualizzate, quindi Discourse crea una versione ridimensionata dell’immagine e la aggiunge al post. Quando fai clic su quell’immagine ridimensionata, appare una sovrapposizione elegante che contiene l’immagine a dimensioni intere: questo è comunemente chiamato “lightboxing”.
Attualmente, Discourse utilizza una libreria chiamata Magnific Popup per gestire il comportamento del lightboxing. Questo argomento riguarda il rinnovamento dei lightbox in Discourse, migrando da Magnific Popup a qualcosa di più in linea con le aspettative degli utenti e degli sviluppatori di oggi.
Il perché
Magnific è una libreria eccellente, e il numero di stelle sulla pagina del progetto indica significativamente quanti problemi ha risolto per utenti e sviluppatori nel corso degli anni.
Era anche avanti rispetto al suo tempo in termini di facilità d’uso e funzionalità. Questo diventa evidente quando si considera che quasi tutte le funzionalità che vedi oggi erano già presenti nella versione 1.0, rilasciata nel 2014.
Allora, dove ci troviamo? Lo terrò breve. Magnific Popup è stato creato per un mondo completamente diverso, dove la compatibilità tra browser era molto indietro rispetto a oggi. Questo significa quattro cose:
- Ha jQuery come dipendenza
- Contiene codice destinato a browser antichi
- Il dispositivo medio utilizzato per accedere al web è cambiato molto da allora
- È stato progettato con qualcosa di diverso dalle applicazioni a pagina singola come Discourse come priorità, ovvero pagine statiche. Questo comporta preoccupazioni specifiche per le prestazioni delle applicazioni a pagina singola, che affronteremo più avanti.
Data l’attuale stato degli standard web e la possibilità di fare ogni tipo di magia con il JavaScript puro, è comprensibile che progetti di grandi dimensioni come Discourse si stiano allontanando dall’avere jQuery come dipendenza. Si noti che le discussioni su jQuery sono fuori tema qui e questo serve principalmente per fornire contesto.
Quindi, questo è il perché: meno jQuery, meno codice per browser antichi, migliori prestazioni complessive e un migliore supporto per il dispositivo medio, ora molto diverso.
Il come
Non mancano soluzioni disponibili e c’è da discutere sul non reinventare la ruota, ma… non andiamo oltre e manteniamolo breve.
Forse il modo più breve per affrontare questo punto critico è dire… non importa quanto bene si adatti qualcosa pronto all’uso… non si adatterà mai tanto bene quanto qualcosa su misura, e lasciamo stare.
Ci sono diverse cose da considerare qui, sia che si tratti di eccesso di funzionalità con librerie che coprono numerosi casi d’uso come immagini, iframe e video, o mancanza di chiarezza sulle licenze.
Conoscere i casi d’uso specifici che Discourse deve supportare rende possibile evitare codice non necessario, consentendo l’aggiunta di funzionalità mirate senza finire con un eccesso di sovraccarico.
Quindi, ora abbiamo una buona base di partenza. I requisiti sono:
- Nessun jQuery
- Concentrarsi solo sulle immagini per ora
- Supporto per miglioramenti alla qualità della vita come lo scorrimento su mobile
- Integrazione con Discourse per consentire ulteriori personalizzazioni/miglioramenti utilizzando i sistemi attuali di temi e plugin.
Il cosa
Discourse è un negozio Ember, quindi il nuovo lightbox dovrebbe essere un componente Ember per gestire il markup e i dati. Inoltre, poiché c’è l’aspettativa che si possa impostare un lightbox ovunque nell’app, ha molto senso avere un servizio per i lightbox. Questo permetterà agli sviluppatori di:
- Iniettare il servizio in qualsiasi componente e collegare i metodi di impostazione/pulizia del servizio al ciclo di vita del componente
- Cercare il servizio ovunque nell’app e chiamare i suoi metodi di impostazione/pulizia
Inoltre, come miglioramento alla qualità della vita, possiamo aggiungere un po’ di astrazione e creare alcune funzioni di utilità utilizzabili nei temi. Ne parleremo più avanti.
Poiché uno degli obiettivi è consentire a temi e plugin di estendere le funzionalità, il servizio lightbox comunicherà le modifiche di stato tramite appEvents. Emetterà i seguenti eventi:
lightbox:openedlightbox:item-will-changelightbox:item-did-changelightbox:closed
Ogni evento verrà attivato con il tipo di dati che gli sviluppatori si aspetterebbero tipicamente. Ad esempio, l’evento lightbox:opened conterrà un elenco di tutti gli elementi e l’elemento corrente su cui è stato aperto il lightbox. Ne parleremo più avanti.
Detto questo, passiamo oltre.
Negli ultimi settimane, ho lavorato a una bozza di PR che introduce Discourse Lightbox.
A prima vista, potrebbe sembrare piuttosto grande, quindi spezziamola.
Testare qualcosa che dipende da molte interazioni dell’utente è complicato, ecco perché la suite di test del lightbox include 63 test con 291 asserzioni.
Oltre alla dimensione dei test, facciamo un rapido confronto delle dimensioni con Magnific Popup (entrambi non minificati):
| Discourse Lightbox LOC | Magnific Popup LOC | Differenza | |
|---|---|---|---|
| Javascript | 1197 | 1860 | (35%) |
| CSS | 813 | 351 | 131% |
| Templates | 401 | 0 | - |
| Total | 2411 | 2211 | 9.1% |
Quindi, Discourse Lightbox ha circa il 9% di codice in più rispetto a Magnific. Naturalmente, questa è solo parte della storia per due motivi:
- Non abbiamo tenuto conto di jQuery, di cui Magnific dipende. jQuery 3.6 ha circa 11k LOC.
- Discourse Lightbox aggiunge più funzionalità rispetto a Magnific.
Entriamo in queste funzionalità.
Qualsiasi scatto si veda nei video sottostanti è legato alla registrazione, non a ciò che gli utenti sperimenteranno. Tutte le animazioni/transizioni verranno eseguite a un solido 60 FPS.
Quindi, senza ulteriori indugi,
Layout di base
Per confronto, ecco lo stesso nell’attuale implementazione di Magnific:
Alcuni punti sui video sopra:
-
Il nuovo lightbox userà l’immagine come sfondo invece del generico sfondo scuro semitrasparente. L’immagine, per definizione, si completerà da sola. Si noti che questo non aggiunge alcun sovraccarico di rete. L’immagine utilizzata come sfondo è quella del corpo del post, il che significa che sarà già stata memorizzata nella cache quando gli utenti aprono il lightbox.
-
Discourse Lightbox opta per un’interfaccia utente fissa. Invece di avere il pulsante di chiusura, il titolo e i metadati dell’immagine attaccati all’immagine, hanno i loro spazi riservati. Questo aiuterà a ridurre i salti mentre si naviga tra immagini di dimensioni diverse.
-
La navigazione tra le immagini può essere effettuata con le frecce nel lightbox o tramite scorciatoie da tastiera. → o ↓ per il successivo e ← o ↑ per il precedente. Nelle località RTL, le scorciatoie sono invertite di conseguenza.
Il layout su mobile è molto simile, tranne per il fatto che le frecce non vengono visualizzate perché vengono aggiunte le gesture di scorrimento per la navigazione.
Scorri a sinistra su mobile per il successivo e a destra per il precedente. Nelle località RTL, le gesture di scorrimento sono invertite.
Zoom
Il nuovo lightbox ha un pulsante dedicato per lo zoom visibile quando l’immagine che stai visualizzando è più grande delle dimensioni della viewport. Fare clic sul pulsante di zoom ingrandirà l’immagine, e fare clic nuovamente sul pulsante la rimpicciolirà. Inoltre, ora c’è una scorciatoia da tastiera per lo zoom in/out: Z. Infine, se l’immagine è ingrandibile, fare clic su di essa avrà lo stesso effetto.
Ecco un video che dimostra tutti e tre i metodi:
Si noti che, sebbene non dimostrato nel video sopra, il cursore, quando si passa sopra un’immagine ingrandibile, cambierà per rifletterlo. Cambierà anche in un’icona zoom-out quando si passa sopra un’immagine già ingrandita.
Lo zoom funziona diversamente nel nuovo lightbox. Su desktop, la sezione ingrandita dell’immagine seguirà il cursore.
Su mobile non c’è hover; viene utilizzato lo scorrimento touch regolare per muoversi in un’immagine ingrandita.
Rotazione
Il nuovo lightbox aggiunge un pulsante dedicato per ruotare l’immagine di incrementi di 90 gradi. Anche la rotazione ha una scorciatoia da tastiera: il tasto R. Ecco come appare:
Rotazione e zoom possono essere combinati.
Modalità a schermo intero
Il nuovo lightbox aggiunge un pulsante a schermo intero. Il pulsante farà entrare la finestra del browser in modalità a schermo intero. La scorciatoia da tastiera è M.
Mantenerà traccia del suo stato e tornerà alla modalità normale quando lo schermo intero viene disattivato o quando il lightbox viene chiuso.
Download
Il lightbox attuale aggiunge un link “download” sotto l’immagine. Il nuovo lightbox fa lo stesso, ma cambia il link in un’icona e lo aggiunge al piè di pagina del lightbox. Rispetta comunque le stesse autorizzazioni. Se…
prevent_anons_from_downloading_images
è abilitato e l’utente non è loggato, l’icona di download non verrà visualizzata.
Nuova scheda
Il lightbox attuale aggiunge un link “originale” sotto l’immagine che la apre in una nuova scheda. Il nuovo lightbox fa lo stesso, ma lo aggiunge come un’icona nell’intestazione del lightbox. Rispetterà anche le stesse autorizzazioni impostate per l’icona di download.
Titolo dell’immagine
Il nuovo lightbox si concentra principalmente sull’immagine. I titoli delle immagini vengono troncati a una riga per impostazione predefinita, ma supportano l’espansione. Ecco un esempio di come appare:
C’è una scorciatoia da tastiera per espandere/collassare il titolo: T, e il titolo non verrà visualizzato quando l’immagine è ingrandita/ruotata.
Carosello
Una nuova funzionalità disponibile è la visualizzazione di tutte le immagini nella galleria in un carosello. Il layout dipenderà dallo schermo del dispositivo; può essere orizzontale o verticale, ed ecco come appare:
C’è una scorciatoia da tastiera per attivare/disattivare il carosello: A.
Ecco come appare su mobile:
Scorrere verso il basso su mobile attiverà/disattiverà il carosello.
Chiusura
Il tasto Esc funziona ancora come prima su desktop per chiudere il lightbox. Ora c’è una gesture di scorrimento aggiuntiva su mobile: puoi scorrere verso l’alto per chiudere il lightbox.
Accessibilità
Oltre alle basi come le etichette dei pulsanti, il nuovo lightbox aggiunge un elemento annunciatore per screen reader fuori schermo. Quando si naviga a un’immagine all’interno del lightbox, leggerà il suo indice e titolo secondo il formato seguente:
immagine %{current} di %{total}: %{title}
Ecco un breve esempio:
Il nuovo lightbox rimuove anche tutti i pulsanti non necessari che non servono a nulla per gli screen reader tramite aria-hidden.
Ricordate che l’accessibilità è una missione in corso e questo non è assolutamente completo. C’è sempre qualcosa da migliorare, ma mi sono fermato qui per mantenere le cose semplici per la v1.
Questo copre tutte le funzionalità del nuovo lightbox.
Passiamo ora a un po’ di linguaggio da sviluppatore.
Ascoltatori di eventi
Il lightbox attuale aggiunge ascoltatori di eventi click a ogni singola immagine del lightbox nei post elaborati. Ciò significa che un post con 20 immagini avrà 20 ascoltatori di eventi click per i lightbox.
Il nuovo lightbox sfrutta la delega degli eventi e aggiunge un solo ascoltatore di eventi al post stesso.
Ecco il conteggio degli ascoltatori di eventi del lightbox attuale per un utente anonimo in una finestra di navigazione in incognito su un post con 20 immagini dopo aver forzato la raccolta della spazzatura:
Ecco lo stesso post con il nuovo lightbox:
Inoltre, la navigazione all’interno del lightbox attualmente aggiunge ascoltatori di eventi, che finiscono per essere orfani e quindi interferiscono con la raccolta della spazzatura. Ecco un grafico di:
- Caricare una pagina di argomento con un post contenente 20 immagini.
- Aprire il lightbox e navigare attraverso tutte le 20 immagini tre volte consecutive.
- Chiudere il lightbox.
- Forzare la raccolta della spazzatura del browser.
Lightbox attuale:
Nuovo lightbox:
Per quanto riguarda gli ascoltatori di eventi sui lightbox stessi, questi non sembrano essere puliti attualmente in Magnific (ricordate, non è stato costruito per applicazioni a pagina singola).
Una nota veloce sui test sopra. Sono molto rudimentali e i numeri non dovrebbero essere “scientifici”; l’obiettivo qui è capire la direzione e non i numeri esatti.
Note per gli sviluppatori
Parliamo di impostazione e pulizia dei lightbox con il nuovo Discourse Lightbox.
Impostazione e pulizia dei lightbox
Gli sviluppatori hanno due opzioni.
-
Iniettare il servizio lightbox in un componente tramite:
import { inject as service } from "@ember/service"; //... @service lightboxQuindi puoi chiamare:
this.lightbox.setupLightboxes({ container: yourContainer // Nodo DOM selector: ".css-selector" // Selettore stringa per gli elementi che vuoi mettere nel lightbox })Poi, quando vuoi pulire, chiama semplicemente:
this.lightbox.cleanupLightboxes()È tutto.
-
Se non vuoi iniettare il servizio lightbox, puoi importare
setupLightboxesecleanupLightboxescosì:import { cleanupLightboxes, setupLightboxes, } from "discourse/lib/lightbox";Il resto è lo stesso dell’iniezione del servizio. Queste due funzioni cercheranno il servizio per te. Quindi:
setupLightboxes({ container: yourContainer // Nodo DOM selector: ".css-selector" // Selettore stringa per gli elementi che vuoi mettere nel lightbox }) //.... cleanupLightboxes()
Si noti che sia chiamando direttamente dal servizio che tramite le funzioni di utilità verrà accettata anche una nodeList per compatibilità con le versioni precedenti, ma non è raccomandato.
Un’ultima nota: puoi anche avere un elemento non immagine come trigger per aprire un lightbox che hai impostato. Ad esempio:
<div class="my-container">
<img class="my-selector" src="foo">
<img class="my-selector" src="bar">
....
<button>Apri Lightbox</button>
</div>
Farei qualcosa del genere per impostare lightbox di base sul div sopra:
import {
cleanupLightboxes,
setupLightboxes,
} from "discourse/lib/lightbox";
//...
setupLightboxes({
container: document.querySelector(".my-container"),
selector: ".my-selector"
})
Per far sì che il pulsante apra il lightbox, tutto ciò che devi fare è aggiungere data-lightbox-trigger così:
<button data-lightbox-trigger>Apri Lightbox</button>
Il resto è gestito automaticamente.
Infine, quando vuoi pulire, chiama:
cleanupLightboxes()
La pulizia non è davvero critica poiché il servizio lightbox pulirà automaticamente ogni volta che l’evento dom:clean viene attivato nell’app (durante le transizioni di route).
Ascolto degli eventi del lightbox
Il nuovo lightbox attiverà eventi, come abbiamo discusso prima. Questi eventi sono:
lightbox:openedlightbox:item-will-changelightbox:item-did-changelightbox:closed
lightbox:opened
Questo evento verrà attivato quando il lightbox viene aperto e contiene due oggetti.
items: questo è un array di tutte le immagini nel lightbox corrente. Ognuna di esse sarà un oggetto.currentItem: questo è l’oggetto per l’elemento corrente su cui è stato aperto il lightbox.
Un oggetto elemento appare così:
{
"fullsizeURL": "https://d11a6trkgmumsb.cloudfront.net/original/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff.jpeg",
"smallURL": "https://d11a6trkgmumsb.cloudfront.net/optimized/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff_2_600x750.jpeg",
"downloadURL": "/uploads/short-url/56rKTvkvmL6c2C8OFQtMo3D8sIn.jpeg?dl=1",
"title": "Foto ravvicinata di un microfono nero su supporto",
"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://d11a6trkgmumsb.cloudfront.net/optimized/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff_2_600x750.jpeg);",
"isLoaded": true,
"hasLoadingError": false,
"width": 1200,
"height": 1500,
"canZoom": true
}
lightbox:item-will-change
Questo evento viene attivato subito prima che l’elemento corrente nel lightbox cambi. Avrà currentItem (quello che sta per cambiare).
lightbox:item-did-change
Questo evento viene attivato subito dopo che l’elemento nel lightbox cambia e ha finito di caricare, e avrà currentItem come argomento.
lightbox:closed
Questo evento viene attivato subito dopo che il lightbox viene chiuso e non ha argomenti.
Con gli eventi sopra, un componente di tema teorico può facilmente aggiungere analisi al lightbox così:
api.onAppEvent('lightbox:opened', ({items, currentItem}) => {
console.log({items});
console.log({currentItem});
// Il tuo codice di analisi qui
});
o altre idee simili.
Il CSS
Il nuovo lightbox utilizza la convenzione di denominazione BEM per le classi HTML. Ecco un elenco completo dei selettori che puoi utilizzare:
html.has-lightbox {
// CSS per l'elemento HTML quando i lightbox sono aperti
}
.d-lightbox {
&--is-visible {
// Elemento principale del lightbox
}
&__content {
// Contenitore del contenuto interno del lightbox
}
}
.d-lightbox {
&__content__header {
// Intestazione del lightbox
}
}
.d-lightbox {
&__content__body {
// Corpo del lightbox (contiene l'immagine principale)
&__backdrop {
// Sfondo del lightbox
}
&__main-image {
// Immagine principale del lightbox
}
&__error-message {
// Messaggio di errore del lightbox
}
&__previous-button,
&__next-button {
// Pulsanti precedente/successivo principali del lightbox
}
}
}
.d-lightbox {
&__content__footer {
// Piè di pagina del lightbox
&__main-title {
// Titolo dell'immagine del lightbox
&__item-file-details {
// Dettagli file del lightbox, es. "1000x582 183KB"
}
}
}
}
.d-lightbox {
&__content__carousel {
// Contenitore del carosello del lightbox
&__previous-button,
&__next-button {
// Pulsanti precedente/successivo del carosello del lightbox
}
}
}
.d-lightbox {
&__content__carousel {
&__carousel-items {
// Contenitore degli elementi del carosello del lightbox
&__item,
&__item--is-current {
// Elemento del carosello del lightbox
}
&__item--is-current {
// Elemento corrente del carosello del lightbox
}
}
}
}
.d-lightbox {
&--is-vertical &__content__carousel {
// Stili verticali del carosello del lightbox
}
}
.d-lightbox {
&--is-horizontal &__content__carousel {
// Stili orizzontali del carosello del lightbox
}
}
.d-lightbox {
.btn-flat {
// Stili per tutti i pulsanti del lightbox
}
}
.d-lightbox {
&__content {
&__focus-trap,
&__screen-reader-announcer {
// Trappola per il focus e annunciatore per screen reader del lightbox. Questi sono fuori schermo
}
}
}
/* Stili di stato */
// Carosello
.d-lightbox {
&--has-carousel {
// Stili del lightbox quando il carosello è aperto
}
}
// Titolo espanso
.d-lightbox {
&--has-expanded-title {
// Stili del lightbox quando il titolo è espanso
}
}
// Zoom
.d-lightbox {
&--can-zoom {
// Stili del lightbox quando l'immagine può essere ingrandita
}
&--is-zoomed {
// Stili del lightbox quando l'immagine è ingrandita
}
}
// Rotazione
.d-lightbox {
&--is-rotated {
// Stili del lightbox quando l'immagine è ruotata
}
}
// Schermo intero
.d-lightbox {
&--is-fullscreen {
// Stili del lightbox quando l'immagine è a schermo intero
}
}
Ora che abbiamo coperto il cosa, possiamo finalmente passare a…
Il quando
Al momento, la PR è pronta per la revisione, che deve avvenire per prima. Dopo aver superato la revisione, sarà disponibile per i siti che aggiornano. La PR aggiunge una nuova impostazione del sito temporanea mentre transitiamo da Magnific Popup a Discourse Lightbox. Il nome dell’impostazione è:
enable_experimental_lightbox
Se l’impostazione è disabilitata, la PR non avrà alcun effetto e tutto continuerà a funzionare come prima con Magnific Popup.
Discourse Lightbox sostituirà Magnific Popup nei post elaborati, nei messaggi di chat e nei componenti di caricamento delle immagini quando l’impostazione viene attivata.
Roadmap
- Revisione della PR
- Fusione della PR
- Finestra per feedback generali (1-2 settimane)
- PR per rimuovere Magnific Popup dal core e rimuovere l’impostazione del sito sperimentale.
- Da definire: obiettivi di estensione, come un componente di tema che esplora layout diversi (dovrebbe essere semplice dato che il nuovo lightbox utilizza CSS grid per il layout).
Ringraziamenti
- Questo lavoro è stato generosamente sponsorizzato da CDCK

- Un grande amore
va a Dmytro Semenov, creatore di Magnific Popup, per aver creato qualcosa che era molto avanti rispetto al suo tempo. - Le immagini utilizzate nelle demo sopra sono cortesemente fornite da Irina Iriser @pexels




