Source: sections/section.js


import $      from '$qui/lib/jquery.module.js'
import Logger from '$qui/lib/logger.module.js'

import {NotImplementedError} from '$qui/base/errors.js'
import {AssertionError}      from '$qui/base/errors.js'
import {mix}                 from '$qui/base/mixwith.js'
import SingletonMixin        from '$qui/base/singleton.js'
import StockIcon             from '$qui/icons/stock-icon.js'
import * as MenuBar          from '$qui/main-ui/menu-bar.js'
import * as TopBar           from '$qui/main-ui/top-bar.js'
import * as Navigation       from '$qui/navigation.js'
import {getCurrentContext}   from '$qui/pages/pages.js'
import {setCurrentContext}   from '$qui/pages/pages.js'
import * as Window           from '$qui/window.js'

import * as Sections from './sections.js'


/**
 * The base class for sections.
 * @alias qui.sections.Section
 * @mixes qui.base.SingletonMixin
 */
class Section extends mix().with(SingletonMixin) {

    /**
     * @constructs
     * @param {String} id section identifier
     * @param {String} title section title
     * @param {qui.icons.Icon} icon section icon
     * @param {String} [buttonType] one of:
     *  * {@link qui.sections.BUTTON_TYPE_NONE}
     *  * {@link qui.sections.BUTTON_TYPE_MENU_BAR} (default)
     *  * {@link qui.sections.BUTTON_TYPE_TOP_BAR}
     * @param {Boolean} [closeMainPageOnHide] set to `true` to close the main page when section is hidden
     * {@link qui.sections.BUTTON_TYPE_TOP_BAR} and {@link qui.sections.BUTTON_TYPE_NONE}
     * @param {Number} [index] sets the section position (ordering) in relation with other sections; by default,
     * sections are positioned based on their registration order
     */
    constructor({
        id,
        title,
        icon,
        buttonType = Sections.BUTTON_TYPE_MENU_BAR,
        index = 0,
        closeMainPageOnHide = false
    }) {

        super()

        this._id = id
        this._title = title
        this._icon = icon
        this._buttonType = buttonType
        this._index = index
        this._closeMainPageOnHide = closeMainPageOnHide

        this._mainPage = null
        this._savedPagesContext = null
        this._whenPreloaded = null
        this._whenLoaded = null

        this.logger = Logger.get(`qui.sections.${this._id}`)

        this._button = this._makeSectionButton()
        this._button.on('click', function () {

            if (this.isCurrent()) {
                return
            }

            Sections.switchTo(this, /* source = */ 'button', /* historyEntry = */ true)

        }.bind(this))

        /* Hide menu bar after selecting a menu entry */
        this._button.on('pointerup', function () {
            if (MenuBar.isOpened()) {
                MenuBar.close()
            }
        })

        this._applyIcon()
    }

    /**
     * Return the id of this section.
     * @returns {String}
     */
    getId() {
        return this._id
    }

    /**
     * Override this to implement how the section is preloaded (i.e. before pushing the main page).
     *
     * During the preload execution, this section should not be assumed to be the current section. Preloading blocks
     * navigation by blocking switching to this section.
     *
     * By default, returns a resolved promise.
     *
     * @returns {Promise}
     */
    preload() {
        return Promise.resolve()
    }

    /**
     * Override this to implement how the section is loaded, after pushing the main page.
     *
     * Loading the main page and loading the section may happen concurrently.
     *
     * By default, returns a resolved promise.
     *
     * @returns {Promise}
     */
    load() {
        return Promise.resolve()
    }

    /**
     * Return a promise that settles as soon as the section is preloaded.
     *
     * This method calls {@link qui.sections.Section#preload} once per section instance.
     *
     * @returns {Promise}
     */
    whenPreloaded() {
        if (!this._whenPreloaded) {
            this._whenPreloaded = this.preload()
        }

        return this._whenPreloaded
    }

    /**
     * Return a promise that settles as soon as the section is loaded.
     *
     * This method calls {@link qui.sections.Section#load} once per section instance.
     *
     * A loaded section can also be assumed to be preloaded.
     *
     * @returns {Promise}
     */
    whenLoaded() {
        return this.whenPreloaded().then(function () {

            if (!this._whenLoaded) {
                this._whenLoaded = this.load()
            }

            return this._whenLoaded

        }.bind(this))
    }

    /**
     * Override this method to prevent accidental closing of the section, to the possible extent. Sections can be closed
     * by default.
     *
     * A section is closed only when application window is closed or reloaded.
     *
     * @returns {Boolean}
     */
    canClose() {
        return true
    }

    /**
     * This method is called when {@link qui.sections.register} is called on this section and is responsible of section
     * setting up after registration.
     */
    handleRegister() {
        this._addSectionButton()
    }

    /**
     * This method is called when {@link qui.sections.unregister} is called on this section and is responsible of
     * cleaning up after section removal.
     */
    handleUnregister() {
        this._button.detach()
    }


    /* Visibility & layout */

    /**
     * Called whenever the section becomes visible.
     * @param {String} source the source why the section has been shown; known sources are:
     *  * `"button"` - the section button has been pressed
     *  * `"navigation"` - {@link qui.navigation.navigate} was used to switch to this section
     *  * `"home"` - {@link qui.sections.showHome} was called
     *  * `"program"` - {@link qui.sections.switchTo} called from program
     * @param {?qui.sections.Section} prevSection the previous section
     */
    onShow(source, prevSection) {
    }

    /**
     * Handle the event of section becoming visible.
     * @param {String} source (see {@link qui.sections.Section#onShow})
     * @param {?qui.sections.Section} prevSection the previous section
     */
    handleShow(source, prevSection) {
        /* Inform all pages of event */
        let context = this.getPagesContext()
        context.getPages().forEach(page => page.handleSectionShow())

        this.onShow(source, prevSection)
    }

    /**
     * Called whenever the section is hidden.
     */
    onHide() {
    }

    /**
     * Handle the event of section becoming hidden.
     * @returns {Promise} a promise that resolves if the section can be hidden and rejected otherwise
     */
    handleHide() {
        let promise = Promise.resolve()
        if (this._mainPage && this._closeMainPageOnHide) {
            promise = this._mainPage.close()
        }

        return promise.then(function () {

            /* Inform all pages of event */
            let context = this.getPagesContext()
            context.getPages().forEach(page => page.handleSectionHide())

            this.onHide()

        }.bind(this))
    }

    /**
     * Called when the screen layout changes.
     * @param {Boolean} smallScreen whether the screen is now a small screen or not
     * @param {Boolean} landscape whether the screen layout is now landscape or portrait
     */
    onScreenLayoutChange(smallScreen, landscape) {
    }

    /**
     * Called whenever the screen size changes.
     * @param {Number} width the new width of the screen
     * @param {Number} height the new height of the screen
     */
    onWindowResize(width, height) {
    }

    /**
     * Called when the window becomes active or inactive.
     * @param {Boolean} active
     * @param active
     */
    onWindowActiveChange(active) {
    }

    /**
     * Called when the window becomes focused or unfocused.
     * @param {Boolean} focused
     * @param focused
     */
    onWindowFocusedChange(focused) {
    }

    /**
     * Called when the options bar is opened or closed, while this section is the current section.
     * @param {Boolean} opened `true` if the bar is opened, `false` otherwise
     */
    onOptionsBarOpenClose(opened) {
    }

    _makeAndPushMainPage() {
        this.logger.debug('creating main page')
        this._mainPage = this.makeMainPage()

        let pagesContext = this.getPagesContext()

        /* Overwrite path id with the section id */
        this._mainPage._pathId = this._id

        /* Unset mainPage on close */
        pagesContext.popSignal.connect(function (page, index) {
            if (index === 0) { /* Main page removed */
                this._mainPage = null
            }
        }.bind(this))

        this._mainPage.pushSelf(pagesContext)
    }

    _show(source, prevSection) {
        this.logger.debug('showing section')

        /* Restore the pages context, if present */
        if (this._savedPagesContext) {
            this.logger.debug('restoring pages context')
            setCurrentContext(this._savedPagesContext)
            this._savedPagesContext = null
        }

        /* Create and push the main page if not already created */
        if (!this._mainPage) {
            this._makeAndPushMainPage()
        }

        this._button.addClass('selected')
        this.handleShow(source, prevSection)

        return this.whenLoaded().then(function () {

            /* Start loading the main page when pushed, but don't wait for result */
            this._mainPage.whenLoaded()

        }.bind(this))
    }

    _hide(historyEntry) {
        this.logger.debug('hiding section')
        return this.handleHide().then(function () {
            this._savedPagesContext = getCurrentContext()
            this._button.removeClass('selected')

            if (historyEntry) {
                Navigation.addHistoryEntry()
            }

            setCurrentContext(null)
        }.bind(this)).catch(function (e) {
            if (e == null) {
                e = new Sections.HideCancelled()
            }

            if (e instanceof Sections.HideCancelled) {
                this.logger.debug('hiding cancelled')
            }

            throw e
        }.bind(this))
    }

    /**
     * Tell if this section is the current section or not.
     * @returns {Boolean}
     */
    isCurrent() {
        return Sections.getCurrent() === this
    }

    /**
     * Return the current page of this section.
     * @returns {?qui.pages.PageMixin}
     */
    getCurrentPage() {
        let context = this.getPagesContext()
        return context ? context.getCurrentPage() : null
    }

    /**
     * Return the pages context of this section.
     * @returns {?qui.pages.PagesContext}
     */
    getPagesContext() {
        /* A hidden section has its pages context stored in _savedPagesContext. A visible (current) section owns the
         * current pages context. */

        if (this.isCurrent()) {
            return getCurrentContext()
        }
        else {
            return this._savedPagesContext
        }
    }


    /* Title, icon & button */

    _makeSectionButton() {
        let button = $('<div></div>', {class: 'qui-base-button qui-section-button'})
        button.append($('<div></div>', {class: 'qui-icon'}))
        button.append($('<span></span>', {class: 'label'}))

        button.addClass(this._id)
        button.css('order', this._index * 10)

        switch (this._buttonType) {
            case Sections.BUTTON_TYPE_MENU_BAR:
                button.find('.label').html(this._title)
                break

            case Sections.BUTTON_TYPE_TOP_BAR:
                button.find('.label').remove()
                button.attr('title', this._title)
                break
        }

        return button
    }

    _addSectionButton() {
        switch (this._buttonType) {
            case Sections.BUTTON_TYPE_MENU_BAR:
                MenuBar.addButton(this._button)
                break

            case Sections.BUTTON_TYPE_TOP_BAR:
                TopBar.addButton(this._button)
                break
        }
    }

    /**
     * Show or hide the section button.
     * @param {Boolean} visible
     */
    setButtonVisibility(visible) {
        this._button.toggle(visible)
    }

    /**
     * Return the section icon.
     * @returns {qui.icons.Icon}
     */
    getIcon() {
        return this._icon
    }

    /**
     * Set the section icon.
     * @param {qui.icons.Icon} icon the new icon
     */
    setIcon(icon) {
        this._icon = icon
        this._applyIcon()
    }

    _applyIcon() {
        let icon = this._icon

        if (icon instanceof StockIcon) {
            switch (this._buttonType) {
                case Sections.BUTTON_TYPE_MENU_BAR:
                    icon = icon.alterDefault({
                        variant: 'interactive',
                        activeVariant: 'interactive',
                        selectedVariant: 'background'
                    })
                    break

                case Sections.BUTTON_TYPE_TOP_BAR:
                    icon = icon.alterDefault({variant: Window.isSmallScreen() ? 'white' : 'interactive'})
                    break
            }
        }

        /* Following css manipulations are a workaround for properly detecting decoration background */
        let origSelected = this._button.hasClass('selected')

        this._button.css('transition', 'none')
        this._button.removeClass('selected')

        this._button.find('.qui-icon').empty()
        icon.applyTo(this._button.find('.qui-icon'))

        this._button.css('transition', '')
        if (origSelected) {
            this._button.addClass('selected')
        }

        this._icon = icon
    }

    /**
     * Return the title of this section.
     * @returns {String}
     */
    getTitle() {
        return this._title
    }

    /**
     * Set the title of this section.
     * @param {String} title
     */
    setTitle(title) {
        this._title = title
        if (this._button) {
            this._button.find('.label').html(this._title)
            this._button.attr('title', this._title)
        }
    }


    /* Pages & navigation */

    /**
     * Override this method to create the main section page.
     * @returns {qui.pages.PageMixin} the section's main page
     */
    makeMainPage() {
        throw new NotImplementedError()
    }

    /**
     * Return the main page of this section.
     * @returns {?qui.pages.PageMixin}
     */
    getMainPage() {
        return this._mainPage
    }

    /**
     * Push a new page onto the section's context. The page is not guaranteed to be pushed by the time the function
     * exists.
     * @param {qui.pages.PageMixin} page the page to be pushed
     * @param {Boolean} [historyEntry] whether to create a new history entry for current page before adding the new
     * page, or not (determined automatically by default, using new page's `pathId`)
     * @returns {Promise} a promise that resolves as soon as the page is pushed, or rejected if the page cannot be
     * pushed
     */
    pushPage(page, historyEntry = null) {
        /* By default, pages with pathId will add a new history entry */
        if (historyEntry == null) {
            historyEntry = !!page.getPathId()
        }

        let context = this.getPagesContext()
        if (!context) {
            throw new AssertionError('Attempt to push page to uninitialized section')
        }

        if (!context.isCurrent()) {
            historyEntry = false
        }

        let currentPage = context.getCurrentPage()
        if (currentPage) {
            return currentPage.pushPage(page, historyEntry)
        }
        else {
            page.pushSelf(context)
            page.whenLoaded() /* Start loading the page automatically when pushed */

            if (historyEntry) {
                Navigation.addHistoryEntry()
            }
            else if (context.isCurrent()) {
                Navigation.updateHistoryEntry()
            }

            return Promise.resolve()
        }
    }

    /**
     * Reset the section to its initial state, closing all pages, including the main page.
     * @returns {Promise} a promise that is resolved as soon as the section is reset and the main page is loaded
     */
    reset() {
        this.logger.debug('resetting section')

        let promise = Promise.resolve()
        if (this._mainPage) {
            /* Close main page and all following pages */
            promise = this._mainPage.close(/* force = */ true)
        }

        this.onReset()

        /* Also re-apply button icon, as theme might have been changed */
        this._applyIcon()

        return promise.then(function () {
            this._mainPage = null
            if (this.isCurrent()) {
                this._makeAndPushMainPage()
                return this.whenLoaded().then(function () {
                    /* Start loading the main page when pushed, but don't wait for result */
                    return this._mainPage.whenLoaded()
                }.bind(this))
            }
        }.bind(this))
    }

    /**
     * Called whenever the section is reset.
     */
    onReset() {
    }

    /**
     * Override this to implement redirects from this section when switching to it using
     * {@link qui.navigation.navigate}. By default it returns this section.
     *
     * @param {String[]} path the requested navigation path
     * @returns {qui.sections.Section|Promise<qui.sections.Section>} the new section
     */
    navigate(path) {
        return this
    }

}


export default Section