Source: utils/visibility-manager.js


import * as Theme from '$qui/theme.js'
import {asap}     from '$qui/utils/misc.js'


/**
 * A class that resolves the problem of waiting an async iteration before applying styles with transitions, after
 * changing the `display` style property.
 * @alias qui.utils.VisibilityManager
 */
class VisibilityManager {

    /**
     * @constructs
     * @param {jQuery} element the element to be managed
     * @param {String} [visibleDisplay] the display style property when element is visible; defaults to `""`
     * @param {String} [hiddenDisplay] the display style property when element is hidden; defaults to `"none"`
     * @param {?String} [visibleClass] the class(es) to add to element when it's visible; defaults to `"visible"`
     * @param {?String} [hiddenClass] the class(es) to add to element when it's hidden; defaults to `"hidden"`
     * @param {Boolean} [widthTransition] set to `true` if width should be transitioned while hiding/showing
     * @param {Boolean} [heightTransition] set to `true` if height should be transitioned while hiding/showing
     * @param {Number} [transitionDuration] the duration of the transition, in milliseconds; defaults to
     * {@link qui.theme.getTransitionDuration()}
     */
    constructor({
        element,
        visibleDisplay = '',
        hiddenDisplay = 'none',
        visibleClass = 'visible',
        hiddenClass = 'hidden',
        widthTransition = false,
        heightTransition = false,
        transitionDuration = Theme.getTransitionDuration()
    }) {

        let currentDisplay = element.css('display') || window.getComputedStyle(element[0]).display

        this._element = element
        this._visibleDisplay = visibleDisplay != null ? visibleDisplay : currentDisplay
        this._hiddenDisplay = hiddenDisplay
        this._visibleClass = visibleClass
        this._hiddenClass = hiddenClass
        this._widthTransition = widthTransition
        this._heightTransition = heightTransition

        this._transitionDuration = transitionDuration

        /* Autodetect initial visibility */
        this._visibleAfterTransition = !this._hiddenDisplay || (currentDisplay !== this._hiddenDisplay)

        this._transitionHandle = null
        this._asapHandle = null
    }

    /**
     * Show element.
     */
    showElement() {
        if (this._visibleAfterTransition) {
            return
        }
        this._visibleAfterTransition = true

        if (this._transitionHandle) {
            clearTimeout(this._transitionHandle)
            this._transitionHandle = null
        }
        if (this._asapHandle) {
            clearTimeout(this._asapHandle)
            this._asapHandle = null
        }

        this._element.css('display', this._visibleDisplay)

        /* If element is not yet part of DOM, don't bother with transitions */
        if (!this._isAddedToDOM()) {
            this._element.addClass(this._visibleClass || '')
            this._element.removeClass(
                `${this._hiddenClass || ''} qui-visibility-manager-hidden-width qui-visibility-manager-hidden-height`
            )
            if (this._widthTransition) {
                this._element.css('width', '')
            }
            if (this._heightTransition) {
                this._element.css('height', '')
            }

            return
        }

        /* Handle special case where element has been hidden while not part of DOM and thus has width/height 0px */
        if (this._widthTransition && this._element.css('width') === '0px') {
            this._element.css('width', '')
            this._element.removeClass('qui-visibility-manager-hidden-width')
            this._element.css('width', this._element.width())
            this._element.addClass('qui-visibility-manager-hidden-width')
        }
        if (this._heightTransition && this._element.css('height') === '0px') {
            this._element.css('height', '')
            this._element.removeClass('qui-visibility-manager-hidden-height')
            this._element.css('height', this._element.height())
            this._element.addClass('qui-visibility-manager-hidden-height')
        }

        this._asapHandle = asap(function () {

            this._asapHandle = null
            this._element.addClass(this._visibleClass || '')
            this._element.removeClass(
                `${this._hiddenClass || ''} qui-visibility-manager-hidden-width qui-visibility-manager-hidden-height`
            )

            this._transitionHandle = setTimeout(function () {

                this._transitionHandle = null
                if (this._widthTransition) {
                    this._element.css('width', '')
                }
                if (this._heightTransition) {
                    this._element.css('height', '')
                }

            }.bind(this), this._transitionDuration)

        }.bind(this))
    }

    /**
     * Hide element.
     */
    hideElement() {
        if (!this._visibleAfterTransition) {
            return
        }
        this._visibleAfterTransition = false

        if (this._transitionHandle) {
            clearTimeout(this._transitionHandle)
            this._transitionHandle = null
        }
        if (this._asapHandle) {
            clearTimeout(this._asapHandle)
            this._asapHandle = null
        }

        if (this._widthTransition) {
            this._element.css('width', this._element.width())
        }
        if (this._heightTransition) {
            this._element.css('height', this._element.height())
        }

        /* If element is not yet part of DOM, don't bother with transitions */
        if (!this._isAddedToDOM()) {
            this._element.removeClass(this._visibleClass || '')
            this._element.addClass(this._hiddenClass || '')
            if (this._widthTransition) {
                this._element.addClass('qui-visibility-manager-hidden-width')
            }
            if (this._heightTransition) {
                this._element.addClass('qui-visibility-manager-hidden-height')
            }
            this._element.css('display', this._hiddenDisplay)

            return
        }

        let hideAfterTransition = function () {

            this._asapHandle = null
            this._element.removeClass(this._visibleClass || '')
            this._element.addClass(this._hiddenClass || '')
            if (this._widthTransition) {
                this._element.addClass('qui-visibility-manager-hidden-width')
            }
            if (this._heightTransition) {
                this._element.addClass('qui-visibility-manager-hidden-height')
            }

            this._transitionHandle = setTimeout(function () {

                this._transitionHandle = null
                this._element.css('display', this._hiddenDisplay)

            }.bind(this), this._transitionDuration)

        }.bind(this)

        if (this._widthTransition || this._heightTransition) {
            this._asapHandle = asap(hideAfterTransition)
        }
        else {
            hideAfterTransition()
        }
    }

    /**
     * Tell if element is visible or not.
     * @returns {Boolean}
     */
    isElementVisible() {
        return this._visibleAfterTransition
    }

    _isAddedToDOM() {
        return this._element.parents('body').length > 0
    }

}

export default VisibilityManager