src/components/AbstractLayer.js

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

const {Component} = goog.require('clulib.cm');

const ResizeProvider = goog.require('nbsrc.provider.ResizeProvider');
const {LayerProvider,LayerModel,LayerEvent,LayerEventType} = goog.require('gep.provider.LayerProvider');
const AutoResetProvider = goog.require('gep.provider.AutoResetProvider');
const CursorProvider = goog.require('gep.provider.CursorProvider');

const {listen,unlisten,Event,EventType} = goog.require('goog.events');
const classlist = goog.require('goog.dom.classlist');
const dataset = goog.require('goog.dom.dataset');

/**
 * Abstract class for all layer components. Provides methods and variables that are valid for all layers, such as open, close, update, change content, etc.
 * Component is used from {@Link https://www.npmjs.com/package/clulib}
 * @extends {Component}
 */
class AbstractLayer extends Component
{
    /**
     * @param {string} type Use {@Link LayerType}
     */
    constructor(type)
    {
        super();

        /**
         * Defines which type this layer component is. Use {@Link LayerType}.
         * @type {string}
         * @protected
         */
        this.type_ = type;

        /**
         * Reference to ResizeProvider for monitoring device size changes
         * @type {ResizeProvider}
         * @protected
         */
        this.resizeProvider_ = ResizeProvider.getInstance();

        /**
         * Reference to LayerProvider for monitoring and controlling layer appearance and disappearance.
         * @type {LayerProvider}
         * @protected
         */
        this.layerProvider_ = LayerProvider.getInstance();

        /**
         * Reference to AutoResetProvider to monitor and control the automatic reset of the page when the user is inactive for a specified period of time.
         * @type {AutoResetProvider}
         * @protected
         */
        this.autoResetProvider_ = AutoResetProvider.getInstance();

        /**
         * Reference to CursorProvider for monitoring and controlling the visibility of the custom mouse cursor object.
         * @type {CursorProvider}
         * @protected
         */
        this.cursorProvider_ = CursorProvider.getInstance();

        /**
         * Layer model which is passed by the {@Link LayerEvent} from the {@Link LayerProvider} to this component.
         * @type {LayerModel}
         * @protected
         */
        this.layerModel_ = null;

        /**
         * Data object which holds the values passed by the {@Link LayerEvent} from the {@Link LayerProvider} to this component.
         * @type {*}
         * @protected
         */
        this.layerData_ = null;

        /**
         * List of all layer content elements
         * @type {NodeList}
         * @protected
         */
        this.contents_ = null;

        /**
         * Active layer content id. All layer content dom elements (class `layer-content`) have to include the attribute `data-content=id`.
         * @type {string|null}
         * @protected
         */
        this.activeContentId_ = null;

        /**
         * Active layer content element
         * @type {Element}
         * @protected
         */
        this.content_ = null;

        /**
         * List of dom elements defined by the query selector `button.layer-content-switcher`.
         * @type {NodeList}
         * @protected
         */
        this.contentSwitchers_ = null;

        /**
         * Dom element of the close button
         * @type {NodeList}
         * @protected
         */
        this.closeButtons_ = null;
    }

    /**
     * Component is ready and had loaded all dependencies (inherit method waitFor and sub components).
     * Init the main listeners for monitoring layer state and window size changes.
     * @inheritDoc
     */
    onInit()
    {
        super.onInit();

        this.initContentElements_();

        listen(this.layerProvider_, LayerEventType.SHOW_LAYER, this.handleShowLayer_, false, this);
        listen(this.layerProvider_, LayerEventType.UPDATE_LAYER, this.handleUpdateLayer_, false, this);
        listen(this.layerProvider_, LayerEventType.HIDE_LAYER, this.handleHideLayer_, false, this);
        listen(this.resizeProvider_, EventType.RESIZE, this.handleResize_, false, this);
    }

    /**
     * Builds/finds in the dom of the component the layer contents and selects/displays the active content.
     * @protected
     */
    initContentElements_()
    {
        this.contents_ = /** @type {NodeList} */ (this.getElement().querySelectorAll('.layer-content'));
        this.activeContentId_ = dataset.has(this.getElement(), 'content') ? dataset.get(this.getElement(), 'content') : this.contents_.length > 0 && dataset.has(this.contents_[0], 'content') ? dataset.get(this.contents_[0], 'content') : null;
        this.content_ = this.activeContentId_ ? this.getElement().querySelector('.layer-content[data-content="'+this.activeContentId_+'"]') : this.contents_[0];
        this.contentSwitchers_ = /** @type {NodeList} */ (this.getElement().querySelectorAll('button.layer-content-switcher'));
        this.closeButtons_ = /** @type {NodeList} */ (this.getElement().querySelectorAll('.layer-close'));
    }

    /**
     * Handle layer close click
     * @param {Event} event
     * @protected
     */
    handleCloseClick_(event)
    {
        this.layerProvider_.hide(this.layerModel_.name);
        this.autoResetProvider_.start();
    }

    /**
     * Hides and deactivates the layer by the {@Link LayerEvent} `SHOW_LAYER` from the {@Link LayerProvider}.
     * Initializes dynamic content creation if the content does not already represent the desired content.
     * @param {LayerEvent} event
     * @protected
     */
    handleShowLayer_(event)
    {
        if(event.layer.type != this.type_)
            return;

        if(this.checkLayerContentInitalisation_(event))
        {
            if(this.layerModel_)
            {
                this.deactivate_();
                this.deactivateContent_();
            }

            this.initContent_(event.layer, event.data).then(() => {
                this.activate_();
                this.activateContent_();
            });
        }
        else
        {
            this.activate_();
            this.activateContent_();
        }

        setTimeout(() => {
            classlist.add(this.getElement(), 'is-visible');
        }, 0);
    }

    /**
     * Takes care of changes triggered by the {@Link LayerEventType} `UPDATE_LAYER` event of the {@Link LayerProvider}.
     * @param {LayerEvent} event
     * @protected
     */
    handleUpdateLayer_(event)
    {
    }

    /**
     * Checks whether a content change and thus an update of
     * the content should take place when the layer is called up again.
     * @param {LayerEvent} event
     * @protected
     */
    checkLayerContentInitalisation_(event)
    {
        return !(this.layerModel_ && this.layerModel_.name == event.layer.name)
    }

    /**
     * Hides and deactivates the layer by the {@Link LayerEvent} `HIDE_LAYER` from the {@Link LayerProvider}.
     * @param {LayerEvent} event
     * @protected
     */
    handleHideLayer_(event)
    {
        if(event.layer && event.layer.type != this.type_)
            return;

        this.deactivate_();
        this.deactivateContent_();
        classlist.remove(this.getElement(), 'is-visible');
    }

    /**
     * Init a switch of the layer content by layer navigation elements.
     * @param {LayerModel} layer
     * @param {*=} data
     * @return {Promise}
     * @protected
     */
    initContent_(layer, data = null)
    {
        this.layerModel_ = layer;
        this.layerData_ = data;

        return Promise.resolve();
    }

    /**
     *
     * @param {Event} event
     * @protected
     */
    handleContentSwitch_(event)
    {
        let contentId = dataset.get(/** @type {Element} */ (event.currentTarget), 'content');
        if(contentId)
            this.switchContent_(contentId);
    }

    /**
     * Switches the display of the current layer content element.
     * Id is assigned to a dom element width class `layer-content` and with the attribute `data-content=id`.
     * @param {string} id
     * @protected
     */
    switchContent_(id)
    {
        if(id == this.activeContentId_)
            return;

        let newContent = this.getElement().querySelector('.layer-content[data-content="'+id+'"]');
        if(!newContent)
            throw new Error('Couldn\'t init layer with content id "'+id+'".');

        this.deactivateContent_();

        classlist.add(newContent, 'is-visible');
        classlist.remove(this.content_, 'is-visible');

        this.content_ = newContent;
        this.activeContentId_ = id;

        this.autoResetProvider_.start();
        this.activateContent_();
    }

    /**
     * Activates all interactive elements in the layer.
     * Enable all known buttons (defined by the query selector `button.layer-content-switcher`) that trigger a content switch.
     * @protected
     */
    activate_()
    {
        setTimeout(() => {classlist.add(this.getElement(), 'can-animate-contents');}, 100);

        if(this.contentSwitchers_)
        {
            for(let i=0; i<this.contentSwitchers_.length; i++)
            {
                listen(this.contentSwitchers_[i], EventType.CLICK, this.handleContentSwitch_, false, this);
            }
        }

        if(this.closeButtons_)
        {
            for(let i=0; i<this.closeButtons_.length; i++)
            {
                listen(this.closeButtons_[i], EventType.CLICK, this.handleCloseClick_, false, this);
            }
        }
    }

    /**
     * Dectivates all interactive elements in the layer.
     * @protected
     */
    deactivate_()
    {
        classlist.remove(this.getElement(), 'can-animate-contents');

        if(this.contentSwitchers_)
        {
            for(let i=0; i<this.contentSwitchers_.length; i++)
            {
                unlisten(this.contentSwitchers_[i], EventType.CLICK, this.handleContentSwitch_, false, this);
            }
        }

        if(this.closeButtons_)
        {
            for(let i=0; i<this.closeButtons_.length; i++)
            {
                unlisten(this.closeButtons_[i], EventType.CLICK, this.handleCloseClick_, false, this);
            }
        }
    }

    /**
     * Activates all interactive elements in the active layer content.
     * @protected
     */
    activateContent_()
    {
        this.cursorProvider_.update();
        this.autoResetProvider_.start();
    }

    /**
     * Dectivates all interactive elements in the active layer content.
     * @protected
     */
    deactivateContent_()
    {
    }

    /**
     * Handle window resize
     * @protected
     */
    handleResize_()
    {
    }
}

exports = AbstractLayer;