src/components/MediaLayer.js

goog.module('gep.components.MediaLayer');

const AbstractLayer = goog.require('gep.components.AbstractLayer');
const {LayerType} = goog.require('gep.provider.LayerProvider');
const {ExhibitionProvider,ExhibitionElementModel} = goog.require('gep.provider.ExhibitionProvider');
const VideoProvider = goog.require('gep.provider.VideoProvider');
const libs = goog.require('gep.net.libs');

const {createDom,getAncestorByClass} = goog.require('goog.dom');
const TagName = goog.require('goog.dom.TagName');
const dataset = goog.require('goog.dom.dataset');
const classlist = goog.require('goog.dom.classlist');
const {listen,listenOnce,unlisten,EventType,Event} = goog.require('goog.events');
const {setStyle} = goog.require('goog.style');
const {Size} = goog.require('goog.math');

/**
 * Component that controls the display of the media layer and its contents.
 * @extends {AbstractLayer}
 */
class MediaLayer extends AbstractLayer
{
    constructor()
    {
        super(LayerType.MEDIA);

        /**
         * Reference to ExhibitionProvider to communicate with the backend and provide and monitor all information that dynamically changes the application.
         * @type {ExhibitionProvider}
         * @private
         */
        this.exhibitionProvider_ = ExhibitionProvider.getInstance();

        /**
         * Reference to VideoProvider for creating and monitoring Vimeo video instances.
         * @type {VideoProvider}
         * @private
         */
        this.videoProvider_ = VideoProvider.getInstance();

        /**
         * Reference to the data model of the artwork selected by the user. The data model contains all the information from the backend.
         * @type {ExhibitionElementModel}
         * @private
         */
        this.artwork_ = null;

        /**
         * Reference to the created Swiper object. More information in the Swiper api documention {@link https://swiperjs.com/swiper-api}
         * @type {Swiper}
         * @private
         */
        this.swiper_ = null;

        /**
         * Reference to an array of buttons in the content of this layer that let create and start the playback of a vimeo video instance.
         * @type {NodeList|Array<Element>}
         * @private
         */
        this.mediaButtons_ = [];
    }

    /**
     * This inherited method can be used to complete additional load processes before initializing the component.
     * In this case the Swiper js library will be loaded. {@link https://swiperjs.com/swiper-api}
     * @inheritDoc
     */
    async waitFor()
    {
        await super.waitFor();
        await libs.loadSwiper();
    }

    /**
     * Component is ready and had loaded all dependencies (inherit method waitFor and sub components).
     * @inheritDoc
     */
    onInit()
    {
        super.onInit();

        /**
         * Dom element in which the (dynamically built) content is added.
         * @type {Element}
         * @private
         */
        this.contentWrapper_ = this.getElement().querySelector('.layer-content-wrapper');
    }

    /**
     * Takes care of changes triggered by the {@Link LayerEventType} `UPDATE_LAYER` event of the {@Link LayerProvider}
     * @inheritDoc
     */
    handleUpdateLayer_(event)
    {
        if(this.swiper_ && this.swiper_.slides.length > event.data['mediaId'])
            this.swiper_.slideTo(event.data['mediaId']);
    }

    /**
     * Dynamically creates the layer content/dom structure to display the swipeable media gallery.
     * @inheritDoc
     */
    async initContent_(layer, data)
    {
        // destroy old video player instances
        let playerContainers = this.contentWrapper_.querySelectorAll('.layer-media-vimeo');
        for(let j=0; j<playerContainers.length; j++)
            this.videoProvider_.deletePlayer(playerContainers[j].id);
        // destoy old swiper instances if exist
        if(this.swiper_)
        {
            unlisten(this.swiper_.navigation.nextEl, EventType.CLICK, this.handleNextSwipeClick_, false, this);
            unlisten(this.swiper_.navigation.prevEl, EventType.CLICK, this.handlePrevSwipeClick_, false, this);
            this.swiper_.destroy();
            this.swiper_ = null;
        }
        // remove the old content
        if(this.content_)
            this.content_.remove();

        await super.initContent_(layer, data);

        if(!this.layerData_)
            throw new Error('Couldn\'t init artwork media with empty data!');

        let elementModel = this.exhibitionProvider_.items[this.layerData_['id']];
        this.artwork_ = elementModel ? elementModel.getElement(this.layerData_['elementId']) : null;
        if(!this.artwork_)
            throw new Error('Couldn\'t init media of artwork:"'+this.layerData_['elementId']+'"!');

        await this.artwork_.loadInfos();

        let info = this.artwork_.infos[this.layerData_['infoId']];
        let swiperSlides = [];
        for(let i = 0; i<info.media.length; i++)
        {
            let slide = createDom(TagName.DIV, {'class': 'swiper-slide'});

            let mediaSource = info.media[i].src;
            let mediaItemContent = [];
            let mediaWrapperContent = [];
            let isVideo = info.media[i].type == 'video';
            let wrapperSize = new Size(0,0);

            // creates an video element
            if (isVideo)
            {
                let videoObject = await this.exhibitionProvider_.loadVideoData(mediaSource).then((obj) => obj);
                mediaSource = videoObject && videoObject['thumbnail_url'] ? videoObject['thumbnail_url'] : 'images/video-thumb.png';
                wrapperSize = new Size(videoObject['width'], videoObject['height']);

                let playIcon = createDom(TagName.SPAN, {'class': 'layer-media-play-icon'});
                mediaWrapperContent.push(playIcon);

                let id = 'el' + i.toString() + '::' + info.media[i].src;
                let mediaVideoBackground = createDom(TagName.DIV, {'class': 'layer-media-vimeo-background'});
                dataset.set(mediaVideoBackground, 'cover', wrapperSize.width+'|'+wrapperSize.height);
                let mediaVideoContainer = createDom(TagName.DIV, {'id': id, 'class': 'layer-media-vimeo'}, mediaVideoBackground);
                dataset.set(mediaVideoContainer, 'vimeoId', info.media[i].src);
                mediaItemContent.push(mediaVideoContainer);
            }

            // creates an image used as a video thumbnail image too
            let media = createDom(TagName.IMG, {
                'src': mediaSource,
                'class': 'layer-media layer-media--' + info.media[i].type
            });
            if(wrapperSize.width > 0)
                dataset.set(media, 'coverSize', wrapperSize.width+'|'+wrapperSize.height);
            mediaWrapperContent.push(media);
            media.setAttribute('draggable', false);

            // creates button and wrapper elements
            let mediaWrapper = createDom(isVideo ? TagName.BUTTON: TagName.DIV, {'class': 'layer-media-wrapper'+(isVideo ? ' media-button' : '')}, mediaWrapperContent);
            if(wrapperSize.width > 0)
                dataset.set(mediaWrapper, 'cover', wrapperSize.width+'|'+wrapperSize.height);
            let mediaContainer = createDom(TagName.DIV, {'class': 'layer-media-container'}, mediaWrapper);
            mediaItemContent.push(mediaContainer);
            let mediaItem = createDom(TagName.DIV, {'class': 'layer-media-item layer-media-item--'+info.media[i].type}, mediaItemContent);
            dataset.set(mediaItem, 'type', info.media[i].type);

            let imageDescription = createDom(TagName.P, {'class': 'layer-media-description'}, info.media[i].description);

            listenOnce(media, EventType.LOAD, (event) => {
                let image = /** @type {Image} */ (event.target);
                let coverSize = new Size(image.naturalWidth, image.naturalHeight);
                if(dataset.has(image, 'coverSize'))
                {
                    let sizeObj = dataset.get(image, 'coverSize').split('|');
                    coverSize = new Size(parseInt(sizeObj[0], 10), parseInt(sizeObj[1], 10));
                    dataset.remove(image, 'coverSize')
                }
                dataset.set(imageDescription, 'cover', coverSize.width+'|'+coverSize.height);
                classlist.add(imageDescription, 'is-visible');
                this.applyCoverLook_(imageDescription, coverSize);
            }, false, this);

            slide.append(mediaItem);
            slide.append(imageDescription);
            swiperSlides.push(slide);
        }

        let swiperWrapper = createDom(TagName.DIV, {'class': 'swiper-wrapper'}, swiperSlides);
        let swiperContainer = createDom(TagName.DIV, {'class': 'swiper'}, swiperWrapper);
        let swiperPagination = createDom(TagName.DIV, {'class': 'swiper-pagination swiper-pagination-fraction'});
        let swiperNextButton = createDom(TagName.DIV, {'class': 'swiper-button-next layer-swiper-button'});
        let swiperPrevButton = createDom(TagName.DIV, {'class': 'swiper-button-prev layer-swiper-button'});

        this.content_ = createDom(TagName.DIV, {'class': 'layer-content is-visible'}, [swiperContainer, swiperPagination, swiperNextButton, swiperPrevButton]);
        this.contentWrapper_.append(this.content_);

        await Promise.resolve();
    }

    /**
     * Initiate a video (autoplay) by clicking on the thumbnail image
     * @param {Event} event
     * @private
     */
    handleMediaClick_(event)
    {
        let itemElement = getAncestorByClass(/** @type {Element} */ (event.currentTarget), 'layer-media-item');
        let container = itemElement.querySelector('.layer-media-vimeo');
        let vimeoId = dataset.get(container, 'vimeoId');
        this.videoProvider_.createPlayer(container.id, container, {
            'id': vimeoId,
            'width': itemElement.offsetWidth,
            'height': itemElement.offsetHeight,
            'autoplay': true,
        });
        classlist.add(itemElement, 'is-playing');
        classlist.add(itemElement, 'has-active-player');
        setTimeout(() => {this.cursorProvider_.update();}, 0);
    }

    /**
     * Activates all interactive elements in the active layer content (buttons, swiper).
     * @inheritDoc
     */
    activateContent_()
    {
        super.activateContent_();

        this.mediaButtons_ = this.contentWrapper_.querySelectorAll('.media-button');
        for(let i=0; i<this.mediaButtons_.length; i++)
            listen(this.mediaButtons_[i], EventType.CLICK, this.handleMediaClick_, false, this);

        let hasSlides = this.artwork_.infos[this.layerData_['infoId']].media.length > 1;
        if(!this.swiper_)
        {
            this.swiper_ = new Swiper(this.content_.querySelector('.swiper'), {
                'initialSlide': this.layerData_['mediaId'],
                'pagination': {
                    'el': '.swiper-pagination',
                    'type': "fraction",
                },
                'navigation': {
                    'nextEl': '.swiper-button-next',
                    'prevEl': '.swiper-button-prev',
                },
                'on': {
                    'slideChange': () => {
                        if(this.swiper_)
                        {
                            this.autoResetProvider_.start();
                            let vimeoEl = this.swiper_.slides[this.swiper_.previousIndex].querySelector('.layer-media-vimeo');
                            if(vimeoEl)
                            {
                                let player = this.videoProvider_.getPlayer(vimeoEl.id);
                                if(player)
                                    player.pause().catch((error) => {console.warn(error);});
                            }
                        }
                    }
                }
            });
        }

        if(hasSlides)
        {
            listen(this.swiper_.navigation.nextEl, EventType.CLICK, this.handleNextSwipeClick_, false, this);
            listen(this.swiper_.navigation.prevEl, EventType.CLICK, this.handlePrevSwipeClick_, false, this);
            this.swiper_.enable();
        }
        else
            this.swiper_.disable();

        this.handleResize_();
    }

    /**
     * Deactivates all interactive elements in the active layer content (buttons, swiper, active videos).
     * @inheritDoc
     */
    deactivateContent_()
    {
        super.deactivateContent_();

        if(this.swiper_) {
            this.swiper_.disable();
            unlisten(this.swiper_.navigation.nextEl, EventType.CLICK, this.handleNextSwipeClick_, false, this);
            unlisten(this.swiper_.navigation.prevEl, EventType.CLICK, this.handlePrevSwipeClick_, false, this);
        }

        let playerContainers = this.contentWrapper_.querySelectorAll('.layer-media-vimeo');
        for(let j=0; j<playerContainers.length; j++)
        {
            let player = this.videoProvider_.getPlayer(playerContainers[j].id);
            if(player)
                player.pause().catch((error) => {console.warn(error);});
        }

        for(let i=0; i<this.mediaButtons_.length; i++)
            unlisten(this.mediaButtons_[i], EventType.CLICK, this.handleMediaClick_, false, this);
        this.mediaButtons_ = [];
    }

    /**
     * Handle click for swiping to next media item
     * @private
     */
    handleNextSwipeClick_()
    {
        if(this.swiper_.isEnd && !this.swiper_.animating)
            this.swiper_.slideTo(0, Math.min(600, this.swiper_.slides.length * 200));
    }

    /**
     * Handle click for swiping to previous media item
     * @private
     */
    handlePrevSwipeClick_()
    {
        if(this.swiper_.isBeginning && !this.swiper_.animating)
            this.swiper_.slideTo(this.swiper_.slides.length - 1, Math.min(600, this.swiper_.slides.length * 200));
    }

    /**
     * Checks whether a content change and thus an update of
     * the content should take place when the layer is called up again.
     * @inheritDoc
     */
    checkLayerContentInitalisation_(event)
    {
        let isSameContent = this.layerData_ != null && event.data != null && this.layerData_['id'] == event.data['id'] && this.layerData_['elementId'] == event.data['elementId'] && this.layerData_['infoId'] == event.data['infoId'];
        return !isSameContent;
    }

    /**
     * Handle resizing of the window to update the functionality of custom cover element look.
     * @inheritDoc
     */
    handleResize_()
    {
        super.handleResize_();

        if(this.swiper_)
        {
            let coverElements = this.content_.querySelectorAll('[data-cover]');
            coverElements.forEach((coverElement) => {
                let coverObj = /** @type {string} */ (dataset.get(/** @type {Element} */ (coverElement), 'cover')).split('|');
                let coverSize = new Size(parseInt(coverObj[0], 10), parseInt(coverObj[1], 10));
                this.applyCoverLook_(/** @type {!Element} */ (coverElement), coverSize);
            });
        }
    }

    /**
     * Apply custom cover element look.
     * @param {!Element} coverElement
     * @param {!Size} coverSize
     * @private
     */
    applyCoverLook_(coverElement, coverSize)
    {
        let parentElement = coverElement.parentElement;
        if(classlist.contains(coverElement, 'layer-media-description'))
            parentElement = parentElement.querySelector('.layer-media-container');
        let parentSize = new Size(parentElement.offsetWidth, parentElement.offsetHeight);

        let newSize = new Size(
            parentSize.width,
            parentSize.width / coverSize.width * coverSize.height
        );
        if(newSize.height > parentSize.height)
            newSize = new Size(
                parentSize.height / coverSize.height * coverSize.width,
                parentSize.height
            );

        if(classlist.contains(coverElement, 'layer-media-description'))
            setStyle(coverElement, {'paddingLeft': Math.round((parentSize.width - newSize.width) * .5) + 'px'});
        else
            setStyle(coverElement, {'width': newSize.width+'px', 'height': newSize.height+'px'});
    }
}

exports = MediaLayer;