Source: media/grid/mediaGallery.js

// external dependencies
import _$ from "justifiedGallery";
import "justifiedGallery/dist/css/justifiedGallery.css"

// factory for jQuery Plugin
const $ = _$();

import { UiElement } from "../../ui/UiElement.js";
import { MediaDataStack, MediaBatchLoader, MediaScrollLoader } from "./utils.js"

export { MediaGallery }

/**
 * Media gallery element loader class.
 * @extends UiElement
 * @author ckraemme <ckraemme@awi.de>
 * @private
 */
class MediaGalleryLoader extends UiElement {

    /**
     * Default configuration of the media stack, batch and scroll loader.
     * @param {Element} target - Target element.
     * @param {Object} contentData - Gallery data object containing the content.
     * @param {Boolean} allowEmptyGallery - Do not throw error if number of gallery items is zero.
     * @param {Element} scrollElement - Scroll element.
    */
    constructor(target, contentData, allowEmptyGallery, scrollElement) {
        super(target, {
            'gallery_critical_state': [],
        })
        this.contentData = contentData;
        this.scrollElement = scrollElement;

        this._getNumberOfMediaGalleryItems(allowEmptyGallery).then((contentLength) => {
            const mediaStackStartSize = 100;
            this.mediaStack = new MediaDataStack(this.getElement(), this.contentData, contentLength);
            this.mediaStack.insertIntoGallery(mediaStackStartSize).then(() => {
                this.batchTotalSize = 150 - mediaStackStartSize;
                this.batchGroupSize = 50;
                this._setupBatchLoader();

                this.scrollGroupSize = 50;
                this.scrollLoadThreshold = 40;
                this.galleryElement = this.getElement();
                this._setupScrollLoader();
            })

            $(this.getElement()).on('jg.complete', function () {
                const mediaGallery = this.getElement();
                const querySelectorImages = '.pswp-gallery__item img:not([class*="lazyload"])';
                const lazyLoadingEventName = 'lazyloading.configured';
                MediaGallery.prepareItemsForLazyLoading(mediaGallery, querySelectorImages, lazyLoadingEventName)
            }.bind(this))
        }).catch(error => {
            this.fire('gallery_critical_state', error);
        })
    }

    /**
     * Set up the loading via scrolling and configure the event when the gallery is ready to insert new items.
     */
    _setupScrollLoader() {
        this.scrollLoaderStack = new MediaScrollLoader(this.mediaStack, this.scrollElement, this.galleryElement);
        this._addLazyLoadingListener();
        this._addScrollListener();
    }

    _addLazyLoadingListener(useCapture = false) {
        this._lazyLoadingListener = function () {
            this.scrollLoaderStack.isLoading = false;
        }.bind(this)
        this.getElement().addEventListener('lazyloading.configured', this._lazyLoadingListener, useCapture)
    }

    _removeLazyLoadingListener(useCapture = false) {
        window.removeEventListener('scroll', this._lazyLoadingListener, useCapture);
    }

    _addScrollListener(useCapture = true) {
        this._scrollListener = function () {
            this.scrollLoaderStack.loadViaScrolling(this.scrollGroupSize, this.scrollLoadThreshold)
        }.bind(this)
        window.addEventListener('scroll', this._scrollListener, useCapture);
    }

    _removeScrollListener(useCapture = true) {
        window.removeEventListener('scroll', this._scrollListener, useCapture);
    }

    removeListeners() {
        this._removeLazyLoadingListener();
        this._removeScrollListener();
    }

    /**
     * Set up loading via batches and configure the event when the gallery is ready to insert new items.
     */
    _setupBatchLoader() {
        this.getElement().addEventListener('lazyloading.configured', function () {
            if (typeof this.batchLoaderStack !== "object")
                this.batchLoaderStack = new MediaBatchLoader(this.mediaStack, this.batchGroupSize, this.batchTotalSize);

            if (this.mediaStack.itemsInGallery < this.mediaStack.maxItemsInGallery)
                this.batchLoaderStack.loadNextBatch().catch(e => { console.error(e) })
        }.bind(this))
    }

    /**
     * Get, process and return number of media gallery items.
     * @param {Boolean} allowEmptyGallery - Do not throw error if number of gallery items is zero.
     * @returns Promise with number of items.
     * @private
     */
    _getNumberOfMediaGalleryItems(allowEmptyGallery) {
        return this.contentData.getItemsQuantity().then((number) => {
            let eventMessage;
            if (!number) {
                if (number == null)
                    eventMessage = 'invalid response of item quantity';
                if (number === 0 && !allowEmptyGallery)
                    eventMessage = 'no items in media gallery';
            }
            if (eventMessage) {
                console.log(eventMessage);
                // A timeout of zero lets the stack finish before firing the callback. Necessary to allow registration of event listeners
                setTimeout(() => this.fire('gallery_critical_state', eventMessage), 0);
                return;
            }
            return number
        })
    }
}


/**
 * A media gallery grid with responsive and lazy image loading and a fullscreen view for single images.
 * @extends MediaGalleryLoader
 * @author ckraemme <ckraemme@awi.de>
 */
class MediaGallery extends MediaGalleryLoader {

    /**
     * Set up a media gallery based on justified gallery, photoswipe and lazysizes.
     * @param {string/HTMLElement} target - Target HTML element, serves as anchor for the media gallery.
     * @param {Object} contentData - Gallery data object containing the content.
     * @param {Boolean} allowEmptyGallery - Do not throw error if number of gallery items is zero.
     * @param {HTMLElement} scrollElement - Element containing the vertical media gallery scrollbar.
     */
    constructor(target, contentData, allowEmptyGallery = false, scrollElement = document.documentElement) {
        super(target, contentData, allowEmptyGallery, scrollElement);

        this.setClass(["pswp-gallery", "media-gallery"]);
        this._setupLazysizes();

        this._initModules().then(() => {
            this._setupJustifiedGallery();
        })
    }

    /**
     * Load dependencies asynchronously.
     * @private
     * @returns {Promise} Promise that is resolved when loaded
     */
    _initModules() {
        return import("lazysizes").then(module => {
            this.modules = {
                lazysizes: module,
            }
            return Promise.resolve();
        });
    }

    /**
     * Init the justified gallery and apply default settings.
     * @private
     */
    _setupJustifiedGallery() {
        // Note: justified gallery does not know the image(s) itself, only their aspect ratios based on image width and image height.
        // JG events: 'jg.resize', 'jg.complete', 'jg.rowflush', i.e., $(this.getElement()).on('jg.complete', function () {}).
        const cssPadding = getComputedStyle(document.querySelector('.gallery-header')).getPropertyValue('padding');
        $(this.getElement()).justifiedGallery({
            rowHeight: 200,
            lastRow: 'nojustify',
            margins: cssPadding,
        });
    }

    /**
     * Set up basic configuration for lazysizes.
     * @private
     */
    _setupLazysizes() {
        // Must be called before importing lazysizes so the settings affect auto initialization.
        // window.lazySizesConfig must be defined before importing the lazysizes module.
        window.lazySizesConfig = window.lazySizesConfig || {};

        // Here, the default attribute names are used.
        lazySizesConfig.srcAttr = 'data-src';
        lazySizesConfig.srcsetAttr = 'data-srcset';

        // Expand the calculated visual viewport area in all directions, so that elements can be loaded before they become visible. The default value is calculated depending on the viewport size of the device (default: 370-500). Recommended range: 300-1000.
        lazySizesConfig.expand = window.innerHeight * window.devicePixelRatio * 3;

        // lazySizesConfig.expFactor (default: 1.5): The expFactor is used to calculate the "preload expand", by multiplying the normal expand with the expFactor which is used to preload assets while the browser is idling (no important network traffic and no scrolling). Reasonable values are between 1.5 and 4 depending on the expand option.
        lazySizesConfig.expFactor = 3;

        // Disable self initialization with 'false' - requires to call lazySizes.init() manually. Consequently calculate justified gallery before lazyloading images.
        lazySizesConfig.init = true;

        // Note: native lazy loading can be combined with lazysizes configuration, when setting attribute loading="lazy" in addition to the class name lazyload. This allows the advantage of native features (like HTTP/2) to be used in addition to lazysizes configuration like expand factor. 
    }

    /**
     * Include the class name 'lazyload' to items selected via query selector and dispatch an event when done.
     * @param {String} targetElement - Target element where query selector is applied and event is dispatched.
     * @param {String} querySelectorExpression - Query selector expression to get the items.
     * @param {String} eventName - Event name that is dispatched when done.
     * @private
     */
    static prepareItemsForLazyLoading(targetElement, querySelectorExpression, eventName) {
        const galleryItems = targetElement.querySelectorAll(querySelectorExpression);
        //console.log('prepare items for lazyloading: ', galleryItems.length);
        galleryItems.forEach(image => {
            image.classList.add('lazyload');
        })
        targetElement.dispatchEvent(new Event(eventName));
    }
}