David DeSandros Masonry.js ist eine weitverbreitete und unter ästhetischen Gesichtspunkten überaus sinnvolle Lösung, egal ob es um Blog-Archive, Galerien oder Shopartikel geht.
So sehr ich dieses durch Pinterest berühmt gewordene Layout auch schätze, ich hatte von Anfang an meine Sorgen damit. Wie konnte ich es implementieren, ohne mit einem von zwei für mich unerträglichen Nebeneffekten leben zu müssen?
Variante 1: Schnell etwas Sichtbares, mit verwirrendem Nachspiel
Wie mir scheint, wird Masonry.js nicht mehr oft auf diese einfache Weise implementiert, obwohl es unter Umständen ein akzeptables Ergebnis hervorbringen kann.
Hierbei baut sich das Grid zuerst ohne Einmischung des Skripts auf der Seite auf. Es ist also unmittelbar etwas sichtbar, und erst wenn alle Bilder vollständig geladen sind, kommt das Masonry-Skript zur Anwendung.
/** * Masonry Variant: Show Items Immediately, Later Load Masonry.js * * Items load immediately and Masonry.js gets applied as * soon as all images in the grid are fully loaded. * * @uses imagesLoaded.js */ $( document ).ready( function() { var $container = $( '.bub-masonry' ); $container.imagesLoaded( function() { // Or use window.load() to also account for iframes. $container.masonry({ itemSelector: 'article' }) }); });
Die Variante ist geradeaus. Es gibt nichts zu tüfteln.
Vorteil: Der Leser bekommt von Anfang an etwas zu sehen.
Nachteil: Die Elemente werden vor den Augen des Lesers neu angeordnet, ein Layout wechselt das andere ab. Ein möglicherweise bereits begonnener Lesefluss wird jäh unterbrochen und seltsam ist so ein Wechsel auf jeden Fall.
Für mich kam diese Variante nie in Frage. Der Layout-Sprung ärgert mich einfach. Erträglich kann ich es mir nur mit reinen Bildergalerien vorstellen.
Variante 2: Warten bis alle Bilder geladen sind
Dieses allerorts empfohlene Setup, bei dem gewartet wird, bis alle Elemente des Grids vollständig geladen sind, führt letztendlich zwar zu einem hübschen Ergebnis, hat aber ebenfalls einen unangenehmen Nebeneffekt: die resultierende Wartezeit.
Dem Leser wird daher meist eine Spinner-Animation aufgezwungen, bis das fertige Layout endlich eingeblendet werden kann.
/** * Masonry Variant: Items Hidden until All Images Loaded * * A spinner element shows until all images on the page are * loaded and Masonry.js can be applied. */ $( document ).ready( function() { var $container = $( '.bub-masonry' ); var $spinner = $( '.bub-spinner' ); //* Setup Masonry. var masonrySetup = function( contentFadeInTime ) { // Use 'visibility: hidden' for container in stylesheet. $spinner.fadeOut( 200 ) // After fade out, hide spinner completely in case window gets resized. setTimeout( function() { $spinner.css( 'display', 'none' ); }, 200); $container.css( 'visibility', 'visible' ).hide().fadeIn( contentFadeInTime ); $container.masonry({ // options itemSelector: 'article', }); }; // End: Masonry setup function. // For multiple column layouts (depending on viewport size) load masonry after // everything else on the page has loaded (prevent snapping articles). var browserWidth = ( window.innerWidth > 0 ) ? window.innerWidth : document.documentElement.clientWidth; if ( browserWidth > 500 ) { // Run Masonry. $( window ).on( 'load', function() { masonrySetup( 750 ); }); } else { // Still setup Masonry in case window gets resized. return; } });
Auch wenn es sich um nur ein oder zwei Sekunden handelt, ist eine programmierte Wartezeit niemals ideal. Spinner-Animationen haben das Web überflutet wie ein Designtrend. Klar haben sie eine wichtige Funktion, aber wo es keinen wirklich guten Grund für einen Spinner gibt, möchte ich für ein paar Jahre auch keinen Spinner mehr sehen.
Ist die Wartezeit länger, etwa aufgrund einer langsamen Internetverbindung oder weil das Grid (manchmal leider notwendige) etwas größere Bilddateien, ja vielleicht sogar eingebettete Videos enthält, wird es für den Leser irgendwann unzumutbar.
Variante 3: Elemente werden nacheinander eingeblendet, sobald sie geladen sind
Es gibt eine naheliegende Lösung für dieses Problem: Zu Beginn müssen alle Elemente ausgeblendet sein. Das erste Element des Grids wird eingeblendet, sobald das Bild oder sonstige sich darin befindende Objekt geladen ist. Dasselbe geschieht anschließend mit dem zweiten Element, dann kommt das dritte dran usw.
Nach einer mehrstündigen Google-Suche war ich verwundert, wie gering die Nachfrage nach einer alternativen Lösung wie dieser offenbar ist. Ich bin auf eine Handvoll Snippets gestoßen, die zwar einen „Ony-by-one“-Ansatz verfolgen, aber entweder die vorgegebene Reihenfolge der Elemente völlig ignorieren oder (im Zuge meiner Tests) andere wirre Resultate hervorgebracht haben.
Schließlich habe ich mich selbst an einer Lösung versucht und bin dabei auf das folgende, für mich bisher problemlos funktionierende Ergebnis gekommen.
/** * Masonry Variant: Reveal Items 1 by 1 * * Items are revealed one by one as soon as they are fully loaded. * Great solution that should be avoided in grids, that have lots * of iframe content (slow). * * Parameters: * - 'containerSelector' (string): HTML selector of Masonry container. * - 'itemSelector' (string): HTML selector of Masonry item. * - 'minViewportWidth' (int): Minimum viewport width pixels for * the function to get applied. */ function setupMasonry( containerSelector, itemSelector, minViewportWidth ) { 'use strict'; var $container = $( containerSelector ); var $items = $container.find( itemSelector ); var itemsArray = $items.toArray(); var minViewportWidth; var count = 0; // The masonry container should be hidden via CSS // and made visible with the script being loaded. $container.css( 'visibility', 'visible' ); // Always apply masonry on window resize. $( window ).on( 'resize', applyMasonry ); // Get exact browser width. var browserWidth = ( window.innerWidth > 0 ) ? window.innerWidth : document.documentElement.clientWidth; // Setup Masonry for minimum viewport width. if ( browserWidth >= minViewportWidth ) { // First hide all Masonry items, then reveal one by one. $items.css( 'visibility', 'hidden' ); // Note: We cannot use 'hide()' or 'display: none', because it makes // images and text look a bit blurry. The items must stay on the page. // 'Opacity' + 'animate()' would work, but css animations are smoother. revealMasonryItems(); } // Masonry function. function applyMasonry() { $container.masonry({ itemSelector: itemSelector }); } // Reveal items one by one. function revealMasonryItems() { var $nextItem = $( itemsArray[count] ); var $nextItemImg = $nextItem.find( 'img' ); // Apply Masonry for every item being loaded. applyMasonry(); // Account for iframe content, e.g. Youtube videos. // Anyway, we shouldn't use iframes in a Masonry wall at all (slow and unreliable). if ( $nextItem.has( 'iframe' ).length ) { containsIframe(); } else containsImagesOrNothing(); function containsIframe() { // Though much slower than 'load', we use 'get' since // it doesn't fail as often (throughout browsers). $.get( $nextItem ).always( function() { showNext(); }); } // Account for images. function containsImagesOrNothing() { $nextItem.imagesLoaded().always( function() { showNext(); }); } // Reveal item and turn to next one. function showNext() { $nextItem.add( $nextItemImg ) .css( 'visibility', 'visible' ) // Add the fade-in effect via css. .addClass( 'masonry-item-visible' ); if ( count < itemsArray.length ) { count += 1; return revealMasonryItems(); } else return; } } }
Die Sichtbarkeit des Masonry-Containers muss auf hidden
gesetzt sein, damit es funktioniert. Außerdem sorgt eine kleine CSS-Animation für einen angenehm sanften Aufbauvorgang des Grids.
.bub-masonry { visibility: hidden; } .bub-masonry article.masonry-item-visible { animation: masonryItemFadeIn 200ms ease-in-out; } @keyframes masonryItemFadeIn { 0% {opacity: 0;} 100% {opacity: 1;} }
Fazit: Masonry-Grids mit gutem Gewissen einsetzbar
Ich hatte zwar bedenken, weil die Masonry-Funktion für jedes Element einzeln aufgerufen wird, allerdings scheinen alle gängigen Browser gut damit zurechtzukommen.
iframe-Inhalte (z.B. Youtube-Videos) werden in diesem Snippet per Ajax geladen, das dauert natürlich seine Zeit, aber keine andere Variante hat bei meinen Experimenten in Chrome, Firefox und Edge derart zuverlässig funktioniert.
Die jQuery-Funktion load()
wäre naheliegend und deutlich schneller als $get
, doch bei mindestens einem von fünf Versuchen hat sie überhaupt nichts geladen.
Masonry-Grids und iframe-Content sind naturgemäß ein ganz schlechtes Gespann. Allerdings müssen Videos nur in seltenen Fällen unbedingt schon in die Artikelvorschau eingebettet sein. Und niemand hat außerdem behauptet, dass die Ziegelbauweise eine Allround-Lösung ist.
Mein Verhältnis zum Ziegel-Grid ist jedenfalls besser denn je. Ein Masonry-Layout ohne Nebenwirkungen. Endlich!