Discourse ermöglicht es Benutzern, Bilder in Beiträgen (und an anderen Stellen) hochzuladen. Manchmal sind diese Bilder zu groß, um sie direkt anzuzeigen, weshalb Discourse eine skalierte Version des Bildes erstellt und diese in den Beitrag einfügt. Wenn Sie auf dieses skalierte Bild klicken, erscheint ein ansprechendes Overlay, das das Originalbild in voller Größe enthält – dies wird allgemein als „Lightboxing“ bezeichnet.
Discourse verwendet derzeit eine Bibliothek namens Magnific Popup, um das Lightboxing-Verhalten zu steuern. Dieses Thema befasst sich mit einer Überarbeitung der Lightboxen in Discourse durch die Migration von Magnific Popup zu einer Lösung, die besser den heutigen Erwartungen von Benutzern und Entwicklern entspricht.
Das Warum
Magnific ist eine großartige Bibliothek, und die Anzahl der Sterne auf der Projektseite zeigt deutlich, wie viele Probleme sie im Laufe der Jahre sowohl für Benutzer als auch für Entwickler gelöst hat.
Sie war auch in Bezug auf Benutzerfreundlichkeit und Funktionsumfang ihrer Zeit voraus. Dies wird deutlich, wenn man bedenkt, dass fast alle Funktionen, die man heute darin sieht, bereits in Version 1.0 enthalten waren, die 2014 veröffentlicht wurde.
Wo stehen wir also? Ich fasse mich kurz. Magnific Popup wurde für eine völlig andere Welt entwickelt, in der die Cross-Browser-Kompatibilität weit hinter dem heutigen Stand lag. Das bedeutet vier Dinge:
- Es hat jQuery als Abhängigkeit.
- Es enthält Code, der für alte Browser gedacht ist.
- Die durchschnittlichen Geräte, über die das Web genutzt wird, haben sich seitdem stark verändert.
- Es wurde mit etwas anderem als Single-Page-Applikationen wie Discourse als Priorität entwickelt – nämlich mit statischen Seiten. Dies bringt performancebezogene Probleme mit sich, die speziell für Single-Page-Applikationen gelten und auf die wir später eingehen werden.
Angesichts des aktuellen Zustands von Web-Standards und der Tatsache, dass man mit Vanilla JavaScript heute allerlei Magie vollbringen kann, ist es nachvollziehbar, dass große Projekte wie Discourse darauf verzichten, jQuery als Abhängigkeit zu haben. Beachten Sie, dass Diskussionen über jQuery hier themenfremd sind; dies dient hauptsächlich dem Kontext.
Das war also das Warum – weniger jQuery, weniger Code für alte Browser, bessere Gesamtleistung und bessere Unterstützung für das mittlerweile sehr unterschiedliche Durchschnittsgerät.
Das Wie
Es gibt keine Mangel an Lösungen, und es ließe sich darüber diskutieren, das Rad nicht neu zu erfinden, aber… lassen wir das und halten es kurz.
Vielleicht ist der kürzeste Weg, diesen Stolperstein zu adressieren, folgender: Egal wie gut eine fertige Lösung passt – sie wird nie so gut passen wie etwas, das maßgeschneidert ist. Damit genug.
Es gibt hier einige Aspekte zu berücksichtigen, sei es eine Überfülle an Funktionen bei Bibliotheken, die zahlreiche Anwendungsfälle abdecken (Bilder, Iframes, Videos usw.), oder Unklarheiten bezüglich der Lizenzierung.
Die Kenntnis der spezifischen Anwendungsfälle, die Discourse unterstützen muss, ermöglicht es, unnötigen Code zu vermeiden und zusätzliche, gezielte Funktionen hinzuzufügen, ohne mit viel Overhead zu enden.
Wir haben also nun eine solide Basis. Die Anforderungen lauten:
- Kein jQuery
- Derzeit nur Fokus auf Bilder
- Unterstützung von Verbesserungen der Benutzerfreundlichkeit wie Wischgesten auf mobilen Geräten
- Integration in Discourse, um weitere Anpassungen/Verbesserungen über das aktuelle Theme-/Plugin-System zu ermöglichen.
Das Was
Discourse ist eine Ember-Anwendung, daher müsste die neue Lightbox eine Ember-Komponente sein, um das Markup und die Daten zu verarbeiten. Da zudem erwartet wird, dass Lightboxes überall in der App eingerichtet werden können, macht ein Lightbox-Service viel Sinn. Dies ermöglicht Entwicklern entweder:
- Den Service in jede Komponente zu injizieren und die Setup-/Cleanup-Methoden des Services an den Komponenten-Lebenszyklus zu koppeln.
- Den Service überall in der App abzurufen und dessen Setup-/Cleanup-Methoden aufzurufen.
Darüber hinaus können wir als Verbesserung der Benutzerfreundlichkeit eine gewisse Abstraktion vornehmen und einige Hilfsfunktionen erstellen, die in Themes verwendet werden können. Mehr dazu später.
Da eines der Ziele darin besteht, Themes/Plugins die Erweiterung der Funktionalität zu ermöglichen, kommuniziert der Lightbox-Service Zustandsänderungen über AppEvents. Er emittiert folgende Events:
lightbox:openedlightbox:item-will-changelightbox:item-did-changelightbox:closed
Jedes Event wird mit den Datentypen ausgelöst, die Entwickler typischerweise erwarten. Beispielsweise enthält das Event lightbox:opened eine Liste aller Elemente und das aktuelle Element, bei dem die Lightbox geöffnet wurde. Mehr dazu später.
Damit sind wir soweit. Gehen wir weiter.
In den letzten Wochen habe ich an einem Entwurf für einen PR gearbeitet, der „Discourse Lightbox“ einführt.
Auf den ersten Blick mag er etwas groß wirken, also zerlegen wir ihn.
Das Testen von etwas, das stark auf Benutzerinteraktion angewiesen ist, ist heikel. Deshalb umfasst die Lightbox-Testsuite 63 Tests mit 291 Assertions.
Nachdem wir die Größe der Tests hinter uns gelassen haben, machen wir einen schnellen Größenvergleich mit Magnific Popup (beide nicht minifiziert):
| Discourse Lightbox LOC | Magnific Popup LOC | Unterschied | |
|---|---|---|---|
| Javascript | 1197 | 1860 | (35%) |
| CSS | 813 | 351 | 131% |
| Templates | 401 | 0 | - |
| Total | 2411 | 2211 | 9.1% |
Discourse Lightbox hat also etwa 9 % mehr Code als Magnific. Natürlich ist das nur ein Teil der Geschichte, und zwar aus zwei Gründen:
- Wir haben jQuery nicht berücksichtigt, von dem Magnific abhängt. jQuery 3.6 hat etwa 11.000 LOC.
- Discourse Lightbox bietet mehr Funktionen als Magnific.
Gehen wir auf diese Funktionen ein.
Alle Ruckler, die Sie in den folgenden Videos sehen, stammen von der Aufnahme und nicht von dem, was Benutzer erleben werden. Alle Animationen/Übergänge laufen mit stabilen 60 FPS.
Also, ohne weitere Verzögerung:
Grundlayout
Zum Vergleich, hier dasselbe in der aktuellen Magnific-Implementierung:
Einige Punkte zu den obigen Videos:
-
Die neue Lightbox verwendet das Bild als Hintergrund anstelle eines generischen halbtransparenten dunklen Hintergrunds. Das Bild ergänzt sich per Definition selbst. Beachten Sie, dass dies keinen zusätzlichen Netzwerk-Overhead verursacht. Das im Hintergrund verwendete Bild ist das aus dem Beitragsinhalt, was bedeutet, dass es bereits gecacht ist, wenn Benutzer die Lightbox öffnen.
-
Discourse Lightbox setzt auf ein festes UI. Anstatt dass der Schließen-Button, der Titel und die Bildmetadaten am Bild angebracht sind, haben sie eigene reservierte Bereiche. Dies hilft, Sprünge zu reduzieren, wenn man zwischen Bildern unterschiedlicher Dimensionen navigiert.
-
Die Navigation zwischen Bildern kann über die Pfeile in der Lightbox oder über Tastenkürzel erfolgen. [kbd]→[/kbd] oder [kbd]↓[/kbd] für das nächste Bild und [kbd]←[/kbd] oder [kbd]↑[/kbd] für das vorherige. In RTL-Lokalen sind die Kürzel entsprechend umgekehrt.
Das Layout auf mobilen Geräten ist sehr ähnlich, außer dass die Pfeile nicht angezeigt werden, da Wischgesten zur Navigation hinzugefügt wurden.
Auf mobilen Geräten nach links wischen für das nächste Bild, nach rechts für das vorherige. In RTL-Lokalen sind die Wischgesten umgekehrt.
Zoomen
Die neue Lightbox verfügt über eine dedizierte Zoom-Taste, die sichtbar ist, wenn das betrachtete Bild größer als die Viewport-Dimensionen ist. Ein Klick auf die Zoom-Taste zoomt das Bild herein, ein weiterer Klick zoomt es heraus. Zusätzlich gibt es nun ein Tastenkürzel zum Zoomen ([kbd]Z[/kbd]). Schließlich hat ein Klick auf das Bild denselben Effekt, wenn es zoombar ist.
Hier ein Video, das alle drei Methoden demonstriert:
Beachten Sie, dass zwar nicht im Video demonstriert, aber der Mauszeiger, wenn Sie über ein zoombares Bild fahren, sich ändert, um dies anzuzeigen. Er ändert sich auch zu einem „zoom-out“-Symbol, wenn Sie über ein bereits gezoomtes Bild fahren.
Das Zoomen funktioniert in der neuen Lightbox anders. Auf Desktop folgt der gezoomte Bereich des Bildes dem Mauszeiger.
Auf mobilen Geräten gibt es kein Hover; es wird das normale Touch-Scrollen verwendet, um sich in einem gezoomten Bild zu bewegen.
Drehen
Die neue Lightbox fügt eine dedizierte Dreh-Taste hinzu, um das Bild in 90-Grad-Schritten zu drehen. Das Drehen hat ebenfalls ein Tastenkürzel: die Taste [kbd]R[/kbd]. So sieht das aus:
Drehen und Zoomen können kombiniert werden.
Vollbildmodus
Die neue Lightbox fügt eine Vollbild-Taste hinzu. Die Taste lässt das Browserfenster in den Vollbildmodus wechseln. Das Tastenkürzel ist [kbd]M[/kbd].
Es behält seinen Zustand bei und kehrt in den normalen Modus zurück, wenn der Vollbildmodus deaktiviert wird oder die Lightbox geschlossen wird.
Herunterladen
Die aktuelle Lightbox fügt einen „Download“-Link unter dem Bild hinzu. Die neue Lightbox macht dasselbe, ändert dies jedoch in ein Symbol und fügt es stattdessen in den Footer der Lightbox ein. Es werden weiterhin dieselben Berechtigungen beachtet. Wenn…
prevent_anons_from_downloading_images
aktiviert ist und der Benutzer nicht eingeloggt ist, wird das Download-Symbol nicht angezeigt.
Neuer Tab
Die aktuelle Lightbox fügt einen „Original“-Link unter dem Bild hinzu, der es in einem neuen Tab öffnet. Die neue Lightbox macht dasselbe, fügt es jedoch als Symbol in den Header der Lightbox ein. Es werden ebenfalls dieselben Berechtigungen wie für das Download-Symbol beachtet.
Bildtitel
Die neue Lightbox konzentriert sich hauptsächlich auf das Bild. Bildtitel werden standardmäßig auf eine Zeile gekürzt, unterstützen aber das Erweitern. Hier ein Beispiel:
Es gibt ein Tastenkürzel zum Erweitern/Einklappen des Titels ([kbd]T[/kbd]), und der Titel wird nicht angezeigt, wenn das Bild gezoomt oder gedreht ist.
Karussell
Eine neu verfügbare Funktion ist die Anzeige aller Bilder in der Galerie in einem Karussell. Das Layout hängt vom Bildschirm des Geräts ab; es kann horizontal oder vertikal sein. So sieht es aus:
Es gibt ein Tastenkürzel zum Umschalten des Karussells: [kbd]A[/kbd].
Und so sieht es auf mobilen Geräten aus:
Auf mobilen Geräten nach unten wischen, um das Karussell ein- oder auszuschalten.
Schließen
[kbd]Esc[/kbd] funktioniert auf Desktop wie zuvor zum Schließen der Lightbox. Auf mobilen Geräten gibt es nun eine zusätzliche Wischgeste: Nach oben wischen, um die Lightbox zu schließen.
Barrierefreiheit
Über das Grundlegende wie Button-Beschriftungen hinaus fügt die neue Lightbox ein Element für Screenreader-Ankündigungen außerhalb des Bildschirms hinzu. Wenn Sie zu einem Bild innerhalb der Lightbox navigieren, liest es den Index und den Titel im folgenden Format vor:
image %{current} of %{total}: %{title}
Hier ein kurzes Beispiel:
Die neue Lightbox entfernt zudem alle unnötigen Buttons, die für Screenreader keinen Zweck erfüllen, über aria-hidden.
Denken Sie daran, dass Barrierefreiheit eine fortlaufende Mission ist und dies keineswegs abgeschlossen ist. Es gibt immer etwas zu verbessern, aber ich habe hier angehalten, um die Dinge für v1 einfach zu halten.
Das deckt alle Funktionen der neuen Lightbox ab.
Gehen wir nun zu etwas Entwicklersprache über.
Event-Listener
Die aktuelle Lightbox fügt Click-Event-Listener für jedes einzelne Lightbox-Bild in gekochten Beiträgen hinzu. Das bedeutet, ein Beitrag mit 20 Bildern hat 20 Click-Event-Listener für Lightboxes.
Die neue Lightbox nutzt Event-Delegation und fügt nur einen Event-Listener für den Beitrag selbst hinzu.
Hier ist die Anzahl der Event-Listener für die aktuelle Lightbox auf einem Beitrag mit 20 Bildern für einen anonymen Benutzer im Inkognito-Modus nach erzwungener Garbage Collection:
Und hier derselbe Beitrag mit der neuen Lightbox:
Darüber hinaus fügt die Navigation innerhalb der Lightbox derzeit Event-Listener hinzu, die am Ende verwaist sind und somit die Garbage Collection beeinträchtigen. Hier eine Grafik:
- Laden einer Themen-Seite mit einem Beitrag, der 20 Bilder enthält.
- Öffnen der Lightbox und dreimaliges Durchlaufen aller 20 Bilder.
- Schließen der Lightbox.
- Erzwungene Garbage Collection im Browser.
Aktuelle Lightbox:
Neue Lightbox:
Was die Event-Listener auf den Lightboxes selbst betrifft: Diese scheinen derzeit in Magnific nicht aufgeräumt zu werden (denken Sie daran, es wurde nicht für Single-Page-Applikationen entwickelt).
Eine kurze Anmerkung zu den obigen Tests: Sie sind sehr rudimentär, und die Zahlen sollen nicht „wissenschaftlich“ sein. Das Ziel hier ist es, die Richtung zu bestimmen, nicht die exakten Zahlen.
Entwicklerhinweise
Lassen Sie uns über das Einrichten und Aufräumen von Lightboxes mit der neuen Discourse Lightbox sprechen.
Einrichten und Aufräumen von Lightboxes
Entwickler haben zwei Optionen:
-
Injizieren des Lightbox-Services in eine Komponente via:
import { inject as service } from "@ember/service"; //... @service lightboxSie können dann aufrufen:
this.lightbox.setupLightboxes({ container: yourContainer // DOM-Node selector: ".css-selector" // String-Selektor für die Elemente, die Sie lightboxen möchten })Wenn Sie aufräumen möchten, rufen Sie einfach auf:
this.lightbox.cleanupLightboxes()Das war’s.
-
Wenn Sie den Lightbox-Service nicht injizieren möchten, können Sie
setupLightboxesundcleanupLightboxeswie folgt importieren:import { cleanupLightboxes, setupLightboxes, } from "discourse/lib/lightbox";Der Rest ist derselbe wie beim Injizieren des Services. Diese beiden Funktionen suchen den Service für Sie. Also:
setupLightboxes({ container: yourContainer // DOM-Node selector: ".css-selector" // String-Selektor für die Elemente, die Sie lightboxen möchten }) //.... cleanupLightboxes()
Beachten Sie, dass sowohl der direkte Aufruf über den Service als auch über die Hilfsfunktionen auch eine Node-Liste zur Abwärtskompatibilität akzeptieren, dies jedoch nicht empfohlen wird.
Eine letzte Anmerkung dazu: Sie können auch ein Nicht-Bild als Trigger für das Öffnen einer Lightbox verwenden, die Sie eingerichtet haben. Zum Beispiel:
<div class="my-container">
<img class="my-selector" src="foo">
<img class="my-selector" src="bar">
....
<button>Open Lightbox</button>
</div>
Ich würde etwas wie folgt tun, um grundlegende Lightboxes auf dem obigen Div einzurichten:
import {
cleanupLightboxes,
setupLightboxes,
} from "discourse/lib/lightbox";
//...
setupLightboxes({
container: document.querySelector(".my-container"),
selector: ".my-selector"
})
Damit der Button die Lightbox öffnet, müssen Sie nur data-lightbox-trigger hinzufügen:
<button data-lightbox-trigger>Open Lightbox</button>
Der Rest wird automatisch erledigt.
Schließlich, wann immer Sie aufräumen möchten, rufen Sie auf:
cleanupLightboxes()
Das Aufräumen ist nicht wirklich kritisch, da der Lightbox-Service automatisch aufräumt, wenn das Event dom:clean in der App ausgelöst wird (bei Routen-Übergängen).
Auf Lightbox-Events lauschen
Die neue Lightbox feuert Events, wie wir bereits diskutiert haben. Diese Events sind:
lightbox:openedlightbox:item-will-changelightbox:item-did-changelightbox:closed
lightbox:opened
Dieses Event wird ausgelöst, wenn die Lightbox geöffnet wird, und enthält zwei Objekte:
items: Dies ist ein Array aller Bilder in der aktuellen Lightbox. Jedes davon ist ein Objekt.currentItem: Dies ist das Objekt für das aktuelle Element, bei dem die Lightbox geöffnet wurde.
Ein Item-Objekt sieht so aus:
{
"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": "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://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
Dieses Event wird direkt vor dem Wechsel des aktuellen Elements in der Lightbox ausgelöst. Es enthält das currentItem (das, das wechseln wird).
lightbox:item-did-change
Dieses Event wird direkt nach dem Wechsel des Elements in der Lightbox und nach dem Abschluss des Ladens ausgelöst und enthält currentItem als Argument.
lightbox:closed
Dieses Event wird direkt nach dem Schließen der Lightbox ausgelöst und hat keine Argumente.
Mit den obigen Events kann eine theoretische Theme-Komponente einfach Analytics für Lightboxes hinzufügen, wie folgt:
api.onAppEvent('lightbox:opened', ({items, currentItem}) => {
console.log({items});
console.log({currentItem});
// Ihr Analytics-Code hier
});
oder andere ähnliche Ideen.
Das CSS
Die neue Lightbox verwendet die BEM Benennungskonvention für HTML-Klassen. Hier ist eine vollständige Liste der Selektoren, die Sie verwenden können:
html.has-lightbox {
// CSS für das HTML-Element, wenn Lightboxes offen sind
}
.d-lightbox {
&--is-visible {
// Haupt-Lightbox-Element
}
&__content {
// Wrapper für den inneren Inhalt der Lightbox
}
}
.d-lightbox {
&__content__header {
// Lightbox-Header
}
}
.d-lightbox {
&__content__body {
// Lightbox-Body (enthält das Hauptbild)
&__backdrop {
// Lightbox-Hintergrund
}
&__main-image {
// Hauptbild der Lightbox
}
&__error-message {
// Fehlermeldung der Lightbox
}
&__previous-button,
&__next-button {
// Haupt-Buttons für vorheriges/nächstes Bild der Lightbox
}
}
}
.d-lightbox {
&__content__footer {
// Lightbox-Footer
&__main-title {
// Bildtitel der Lightbox
&__item-file-details {
// Dateidetails der Lightbox, z. B. „1000x582 183KB“
}
}
}
}
.d-lightbox {
&__content__carousel {
// Container für das Lightbox-Karussell
&__previous-button,
&__next-button {
// Karussell-Buttons für vorheriges/nächstes Bild der Lightbox
}
}
}
.d-lightbox {
&__content__carousel {
&__carousel-items {
// Container für die Karussell-Elemente der Lightbox
&__item,
&__item--is-current {
// Karussell-Element der Lightbox
}
&__item--is-current {
// Aktuelles Karussell-Element der Lightbox
}
}
}
}
.d-lightbox {
&--is-vertical &__content__carousel {
// Vertikale Styles für das Lightbox-Karussell
}
}
.d-lightbox {
&--is-horizontal &__content__carousel {
// Horizontale Styles für das Lightbox-Karussell
}
}
.d-lightbox {
.btn-flat {
// Styles für alle Lightbox-Buttons
}
}
.d-lightbox {
&__content {
&__focus-trap,
&__screen-reader-announcer {
// Focus-Trap und Screenreader-Ankündigung der Lightbox. Diese sind außerhalb des Bildschirms
}
}
}
/* Zustands-Styles */
// Karussell
.d-lightbox {
&--has-carousel {
// Lightbox-Styles, wenn das Karussell offen ist
}
}
// Erweiterter Titel
.d-lightbox {
&--has-expanded-title {
// Lightbox-Styles, wenn der Titel erweitert ist
}
}
// Zoom
.d-lightbox {
&--can-zoom {
// Lightbox-Styles, wenn das Bild gezoomt werden kann
}
&--is-zoomed {
// Lightbox-Styles, wenn das Bild gezoomt ist
}
}
// Drehen
.d-lightbox {
&--is-rotated {
// Lightbox-Styles, wenn das Bild gedreht ist
}
}
// Vollbild
.d-lightbox {
&--is-fullscreen {
// Lightbox-Styles, wenn das Bild im Vollbildmodus ist
}
}
Nachdem wir das Was behandelt haben, können wir endlich zu…
Das Wann
übergehen.
Derzeit ist der PR zur Überprüfung bereit, was zuerst erfolgen muss. Nach bestandener Überprüfung steht er für Sites zur Verfügung, die aktualisieren. Der PR fügt eine neue vorübergehende Site-Einstellung hinzu, während wir von Magnific Popup zu Discourse Lightbox wechseln. Der Name der Einstellung lautet:
enable_experimental_lightbox
Wenn die Einstellung deaktiviert ist, hat der PR keine Wirkung, und alles funktioniert weiterhin wie zuvor mit Magnific Popup.
Discourse Lightbox wird Magnific Popup in gekochten Beiträgen, Chat-Nachrichten und Komponenten für den Bild-Uploader ersetzen, sobald die Einstellung aktiviert ist.
Roadmap
- PR-Überprüfung
- PR-Zusammenführung
- Fenster für allgemeines Feedback (1–2 Wochen)
- PR zum Entfernen von Magnific Popup aus dem Kern und Entfernen der experimentellen Site-Einstellung.
- TBD: Stretch-Ziele wie eine Theme-Komponente, die verschiedene Layouts erkundet (sollte unkompliziert sein, da die neue Lightbox CSS Grid für das Layout verwendet).
Danksagungen
- Diese Arbeit wurde großzügig von CDCK gesponsert

- Viel Liebe
geht an Dmytro Semenov, den Schöpfer von Magnific Popup, für die Schaffung von etwas, das seiner Zeit weit voraus war. - Die in den obigen Demos verwendeten Bilder stammen freundlicherweise von Irina Iriser @pexels




