src/components/ExhibitLayer.js

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

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 {createDom,removeChildren} = 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,unlisten,EventType,Event} = goog.require('goog.events');

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

        /**
         * 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();

        /**
         * {@Link ExhibitionElementModel} which represents the content data of the currently opened or last opened layer.
         * @type {ExhibitionElementModel}
         * @private
         */
        this.artwork_ = 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_ = [];
    }

    /**
     * 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');
    }

    /**
     * Dynamically creates the layer content/dom structure to display the artwork infos.
     * @inheritDoc
     */
    async initContent_(layer, data)
    {
        let playerContainers = this.contentWrapper_.querySelectorAll('.layer-media-vimeo');
        for(let j=0; j<playerContainers.length; j++)
            this.videoProvider_.deletePlayer(playerContainers[j].id);
        removeChildren(/** @type {Node} */ (this.contentWrapper_));

        await super.initContent_(layer, data);

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

        // find exhibition element model by the given extra data values: id = exhibit model id and element id = artwork model id
        let itemModel = this.exhibitionProvider_.items[this.layerData_['id']];
        this.artwork_ = itemModel ? itemModel.getElement(this.layerData_['elementId']) : null;
        if(!this.artwork_)
            throw new Error('Couldn\'t init artwork:"'+this.layerData_['elementId']+'"!');

        await this.artwork_.loadInfos();

        // check if the artwork info has mor than on info content and a submenu will be generated
        let hasSubContents = this.artwork_.infos.length > 1;
        if(hasSubContents)
        {
            let content = createDom(TagName.DIV, {'class': 'layer-content is-visible'});
            dataset.set(content, 'content', 'overview');
            this.contentWrapper_.append(content);

            this.createCloseButton_(content);

            let scrollWrapper = createDom(TagName.DIV, {'class': 'layer-content-scroll-wrapper is-inverted'});
            dataset.set(scrollWrapper, 'cmp', 'minibar-container');
            content.append(scrollWrapper);

            this.artwork_.infos.forEach((info, index) => {
                let icon = createDom(TagName.SPAN, {'class': 'icon'});
                icon.innerHTML = '<svg viewBox="0 0 11 16" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.41421 0.707107C1.80474 0.316583 2.4379 0.316582 2.82843 0.707106L10.2426 8.12132L2.82843 15.5355C2.4379 15.9261 1.80474 15.9261 1.41421 15.5355L0.707107 14.8284C0.316582 14.4379 0.316583 13.8047 0.707107 13.4142L6 8.12132L0.707107 2.82843C0.316582 2.4379 0.316583 1.80474 0.707107 1.41421L1.41421 0.707107Z"/></svg>';
                let label = createDom(TagName.SPAN, {'class': 'label'}, info.name);
                let button = createDom(TagName.BUTTON, {'class': 'icon-button layer-content-switcher layer-content-artwork is-inverted', 'aria-label': 'show artwork details'}, [icon, label]);
                dataset.set(button, 'content', 'artwork' + (index+1));
                scrollWrapper.append(button);
            });
        }

        // create artwork info
        for(let i = 0; i<this.artwork_.infos.length; i++)
        {
            let info = this.artwork_.infos[i];
            let content = createDom(TagName.DIV, {'class': 'layer-content'+(hasSubContents ? ' is-subcontent' : ' is-visible')});
            dataset.set(content, 'content', 'artwork' + (i+1));
            this.contentWrapper_.append(content);

            // if a submenu will be generated create a back button else create a normal close button
            if(hasSubContents)
                this.createBackButton_(content);
            else
                this.createCloseButton_(content);

            let scrollWrapper = createDom(TagName.DIV, {'class': 'layer-content-scroll-wrapper is-inverted'});
            // create a minibar container sub component for custom scroll bars
            dataset.set(scrollWrapper, 'cmp', 'minibar-container');
            content.append(scrollWrapper);

            let headline = createDom(TagName.H2, {'class': 'layer-title layer-artwork-title'}, info.name);
            scrollWrapper.append(headline);

            // create text info
            info.details.forEach((detailText, detailTitle) => {
                if(detailText.trim() != '')
                {
                    let title = createDom(TagName.H3, {'class': 'layer-subtitle'}, detailTitle);
                    scrollWrapper.append(title);

                    let text = createDom(TagName.P, {'class': 'layer-text'});
                    text.innerHTML = detailText;
                    scrollWrapper.append(text);
                }
            });

            // create media elements (image and video)
            for(let j = 0; j<info.media.length; j++)
            {
                let mediaSource = info.media[j].src;
                let mediaItemContent = [];
                let mediaWrapperContent = [];

                // creates an video element
                if(info.media[j].type == 'video')
                {
                    let videoObject = await this.exhibitionProvider_.loadVideoData(mediaSource).then((obj) => obj);
                    mediaSource = videoObject && videoObject['thumbnail_url'] ? videoObject['thumbnail_url'] : 'images/video-thumb.png';

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

                    let id = 'el' + i.toString() + '::' + info.media[j].src;
                    let mediaVideoContainer = createDom(TagName.DIV, {'id': id, 'class': 'layer-media-vimeo'});
                    dataset.set(mediaVideoContainer, 'vimeoId', info.media[j].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[j].type});
                media.setAttribute('draggable', false);
                mediaWrapperContent.push(media);

                // creates button and wrapper elements
                let mediaWrapper = createDom(TagName.BUTTON, {'class': 'layer-media-wrapper media-button'}, mediaWrapperContent);
                mediaItemContent.push(mediaWrapper);
                let mediaZoom = createDom(TagName.BUTTON, {'class': 'layer-media-zoom media-button'});
                mediaItemContent.push(mediaZoom);
                let mediaSpacer = createDom(TagName.DIV, {'class': 'layer-media-spacer'});
                mediaItemContent.push(mediaSpacer);
                let mediaItem = createDom(TagName.DIV, {'class': 'layer-media-item'}, mediaItemContent);
                dataset.set(mediaItem, 'type', info.media[j].type);
                dataset.set(mediaItem, 'infoId', i.toString());
                dataset.set(mediaItem, 'mediaId', j.toString());
                scrollWrapper.append(mediaItem);

                let imageDescription = createDom(TagName.P, {'class': 'layer-media-description is-visible'}, info.media[j].description);
                scrollWrapper.append(imageDescription);
            }
        }

        await this.manager.decorate(this.contentWrapper_).then(() => {
            this.initContentElements_();
        });
    }

    /**
     * Activates all interactive elements in the active layer content (buttons).
     * @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);
    }

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

        this.layerProvider_.hide('media');

        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_ = [];
    }

    /**
     * Initiate a video (autoplay) by clicking on the thumbnail image
     * @param {Event} event
     * @private
     */
    handleMediaClick_(event)
    {
        let button = /** @type {Element} */ (event.currentTarget);
        let parentElement = button.parentElement;
        let type = dataset.get(parentElement, 'type');
        if(type == 'video' && !classlist.contains(button, 'layer-media-zoom'))
        {
            let container = parentElement.querySelector('.layer-media-vimeo');
            let vimeoId = dataset.get(container, 'vimeoId');
            this.videoProvider_.createPlayer(container.id, container, {
                'id': vimeoId,
                'width': parentElement.offsetWidth,
                'autoplay': true
            });
            classlist.add(parentElement, 'is-playing');
            classlist.add(parentElement, 'has-active-player');
            setTimeout(() => {this.cursorProvider_.update();}, 0);
        }
        else
        {
            let infoId = parseInt(dataset.get(parentElement, 'infoId'), 10);
            let mediaId = parseInt(dataset.get(parentElement, 'mediaId'), 10);
            this.layerProvider_.show('media', {'id': this.layerData_['id'], 'elementId': this.artwork_.id.toString(), 'infoId': infoId, 'mediaId': mediaId})
        }
    }

    /**
     * Creates a layer close button dom element
     * @param {Element} container
     * @private
     */
    createCloseButton_(container)
    {
        let button = createDom(TagName.BUTTON, {'class': 'layer-close is-inverted', 'aria-label': 'close exhibit info'});
        button.innerHTML = '<svg class="icon" width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="19.3047" y="21.3344" width="27" height="3" rx="1" transform="rotate(-135 19.3047 21.3344)"/><rect x="21.2129" y="2.12132" width="27" height="3" rx="1" transform="rotate(135 21.2129 2.12132)"/></svg>';
        container.append(button);
    }

    /**
     * Creates a layer back button dom element
     * @param {Element} container
     * @private
     */
    createBackButton_(container)
    {
        let button = createDom(TagName.BUTTON, {'class': 'icon-button layer-content-switcher is-inverted layer-content-back', 'aria-label': 'hide artwork details'});
        button.innerHTML = '<span class="icon"><svg width="24px" height="14px" xmlns="http://www.w3.org/2000/svg"><path d="M23,6.5H2.207l5.146-5.146c0.195-0.195,0.195-0.512,0-0.707s-0.512-0.195-0.707,0l-6,6C0.601,6.692,0.564,6.748,0.539,6.809c-0.051,0.122-0.051,0.26,0,0.382c0.025,0.062,0.062,0.117,0.108,0.163l6,6C6.744,13.451,6.872,13.5,7,13.5s0.256-0.049,0.354-0.146c0.195-0.195,0.195-0.512,0-0.707L2.207,7.5H23c0.276,0,0.5-0.224,0.5-0.5S23.276,6.5,23,6.5z"></path></svg></span>';
        dataset.set(button, 'content', 'overview');
        container.append(button);
    }

    /**
     * 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'];
        return !isSameContent;
    }
}

exports = ExhibitLayer;