Viewing File: /var/cpanel/customizations/whm/includes/slideout.js

(function() {
    /**
     * A place for slideout references to live and play. It provides methods
     * to easily perform operations on all tabs/slideouts at once. It also
     * ensures that the slideout tabs are properly positioned and spaced.
     *
     * @class SlideoutList
     *
     */
    function SlideoutList() {
        this.slideouts = [];
        this.version = SlideoutList.version;
        this.isVisible = false;
    }

    SlideoutList.version = 1;
    SlideoutList.spaceBetweenTabs = 3;
    SlideoutList.bottomMargin = 35;

    /**
     * Hides the tab for all registered slideouts.
     */
    SlideoutList.prototype.hideAllTabs = function(activeSlideout) {
        this.slideouts.forEach(function(currentSlideout) {
            currentSlideout.hideTab();
        });

        if(this.legacyAnalyticsSlideout) {
            this.legacyAnalyticsSlideout.tab.hide();
        }
    };

    /**
     * Shows the expanded version of the tab for all registered slideouts.
     */
    SlideoutList.prototype.expandAllTabs = function() {
        this.slideouts.forEach(function(currentSlideout) {
            currentSlideout.showExpandedTab();
        });
    };

    /**
     * Shows the collapsed version of the tab for all registered slideouts.
     */
    SlideoutList.prototype.showAllTabs = function() {
        this.slideouts.forEach(function(currentSlideout) {
            currentSlideout.showCollapsedTab();
        });

        if(this.legacyAnalyticsSlideout) {
            this.legacyAnalyticsSlideout.tab.showCollapsed();
        }
    };

    /**
     * Adds items to the SlideoutList and performs a sort afterwards,
     * ensuring that the NPS tab is always on the tab.
     *
     * @param {slideout|slideout[]} slideout  One or more slideouts to add
     */
    SlideoutList.prototype.add = function(slideout) {
        if(Array.isArray(slideout)) {
            this.slideouts = this.slideouts.concat(slideout);
        }
        else {
            this.slideouts.push(slideout);
        }

        // Make sure NPS is always the first item shown
        this.slideouts.sort(function(a, b) {
            return a.id === "nps" ?
                -1 : b.id === "nps" ?
                    1 : 0;
        });

        this.positionAll();
    };

    /**
     * Sets style.bottom for all of the tabs. The NPS tab will always be on top.
     * If a legacy analytics tab is present, all tabs will be positioned in relation
     * to it. That tab is attached to its main content, so it's a little harder to
     * position that one properly, so this strategy avoids that problem.
     */
    SlideoutList.prototype.positionAll = function() {

        /**
         * Do not change positions if items are visible. This may result in
         * tabs not showing up on very slow connections, but for now this is
         * an acceptable compromise.
         */
        if(this.isVisible) {
            return;
        }

        var slideoutTabs = this.slideouts.map(function(slideout) {
            var tabInfo = _getTabMeasurements(slideout.tab);
            tabInfo.elem = slideout.tab;
            tabInfo.id = slideout.id;
            return tabInfo;
        });

        var legacyTab;
        if(this.legacyAnalyticsSlideout && this.legacyAnalyticsSlideout.tab) {
            legacyTab = _getTabMeasurements(this.legacyAnalyticsSlideout.tab.elem);
            legacyTab.elem = this.legacyAnalyticsSlideout.tab.elem;
            legacyTab.isLegacy = true;
            slideoutTabs.push(legacyTab); // Place legacy tabs at the end
        }

        for(var i = slideoutTabs.length - 1; i >= 0; i--) {
            var currentTab = slideoutTabs[i];
            var tabBelowCurrent = slideoutTabs[i + 1];

            /**
             * If a legacy tab exists, it will always be at the end of the
             * list and everything will be positioned around it.
             */
            if(currentTab.isLegacy) {
                // Move the legacy tab down ever so slightly for better aesthetics with the new tabs
                if(getComputedStyle(currentTab.elem).top === "30px") {
                    currentTab.elem.style.top = "35px";
                    currentTab.bottom -= 5;
                }
            }
            else if(!tabBelowCurrent) {
                // For the last tab, position based off the bottom margin
                currentTab.bottom = SlideoutList.bottomMargin;
                currentTab.elem.style.bottom = currentTab.bottom + "px";
            }
            else {
                // For all other tabs, position based off the tab below it
                currentTab.elem.style.bottom = (tabBelowCurrent.bottom + tabBelowCurrent.height + SlideoutList.spaceBetweenTabs) + "px";
            }
        }
    };

    function _getTabMeasurements(tabElem) {
        var tabRect = tabElem.getBoundingClientRect();
        return {
            height: tabRect.height,
            bottom: window.innerHeight - tabRect.bottom,
        };
    }

    /**
     * Stores a reference to the legacy Analytics slideout wrapper for later use
     * and bind listeners for open/close events on it.
     */
    SlideoutList.prototype.setLegacyAnalyticsSlideout = function() {
        var legacySlideout = this.legacyAnalyticsSlideout = CPANEL.legacyAnalyticsWrapper;

        if(legacySlideout) {
            // Open/close tabs in response to the legacy slideout
            legacySlideout.onOpen.subscribe(this.hideAllTabs.bind(this));
            legacySlideout.onClose.subscribe(this.showAllTabs.bind(this));
        }
    };

    /**
     * Initializes all slideouts.
     */
    SlideoutList.prototype.initializeAll = function() {
        this.slideouts.forEach(function(slideout) {
            slideout.animateEntrance();
        });
    };

    /**
     * Tries to initialize the legacy analytics wrapper and set the
     * position for the tabs on DOMContentLoaded.
     */
    SlideoutList.prototype.contentLoadedHandler = function() {
        this.setLegacyAnalyticsSlideout();
        this.positionAll();
    };

    /**
     * Adds listeners and sets a timer for the initial tab animations.
     */
    SlideoutList.prototype.activate = function() {

        var that = this;
        this.activatedItems = this.activatedItems || {};

        // Set initial tab positions on load
        if (document.readyState === "loading") {
            this.activatedItems.contentLoadedHandler = this.contentLoadedHandler.bind(this);
            document.addEventListener('DOMContentLoaded', this.activatedItems.contentLoadedHandler);
        }
        else {
            this.contentLoadedHandler();
        }

        // Reposition tabs on resize
        if(!this.activatedItems.debouncedPositionAll) {
            this.activatedItems.debouncedPositionAll = _debounce(this.positionAll, 100).bind(this);
            window.addEventListener("resize", this.activatedItems.debouncedPositionAll);
        }

        // Kick off the intro animations after a delay
        if(!this.activatedItems.initializeAllTimer) {
            this.activatedItems.initializeAllTimer = setTimeout(function() {
                that.isVisible = true;
                that.initializeAll();
            }, 2000);
        }
    };

    /**
     * Deactivates all listeners and the animation timer set by the activate method.
     */
    SlideoutList.prototype.deactivate = function() {

        if(this.activatedItems.contentLoadedHandler) {
            document.removeEventListener("DOMContentLoaded", this.activatedItem.contentLoadedHandler);
        }

        if(this.activatedItems.debouncedPositionAll) {
            window.removeEventListener("resize", this.activatedItems.debouncedPositionAll);
        }

        if(this.activatedItems.initializeAllTimer) {
            clearTimeout(this.activatedItems.initializeAllTimer);
        }

        delete this.activatedItems;
    };

    /**
     * Creates a new function that wraps a given function in a debouncer
     *
     * @param {function} func   The function to debounce
     * @param {number} delay    The debounce delay
     * @returns {function}   A new function that wraps the original in the debounce logic
     */
    function _debounce(func, delay) {
        var inDebounce;
        return function() {
            var context = this;
            var args = arguments;
            clearTimeout(inDebounce)
            inDebounce = setTimeout(function(){
                func.apply(context, args)
                }, delay);
        }
    }

    /**
     * Initializes a SlideoutList instance and attaches it to the CPANEL namespace.
     * If there is already an existing instance attached with an older implementation,
     * it will replace it.
     *
     * This method also kicks off the initialization routines for all slideouts.
     */
    function init() {
        var cp = window.CPANEL = window.CPANEL || {};
        if(!cp.slideoutList) {
            cp.slideoutList = new SlideoutList();
        }
        else if(cp.slideoutList && cp.slideoutList.version < SlideoutList.version) {
            var existingSlideouts = cp.slideoutList.slideouts;
            cp.slideoutList.deactivate();
            cp.slideoutList = new SlideoutList();
            SlideoutList.add(existingSlideouts);
        }

        cp.slideoutList.activate();
    }

    init();

})();

/**
 * @callback onOpenCallback
 * @description Called when the slideout opens.
 */

/**
 * @callback onCloseCallback
 * @description Called when the slideout closes.
 */

/**
 * @typedef {Object} SlideoutFactoryOptions
 * @property {HTMLElement} mainContainer   The main slideout container.
 * @property {HTMLElement} tab             The slideout tab.
 * @property {HTMLElement} closeButton     The close button within the slideout content container.
 * @property {string} id                   Some unique identification string to differentiate from other slideouts.
 * @property {string} sessionStorageKey    A unique key to place on sessionStorage that will be used to track the cPanel session ID.
 * @property {onOpenCallback} onOpen       A callback that will be run when the slideout opens.
 * @property {onCloseCallback} onClose     A callback that will be run when the slideout closes.
 */

/**
 * @typedef {Object} Slideout
 * @property {string} id                   Some unique identification string to differentiate from other slideouts.
 * @property {HTMLElement} mainContainer   The main slideout container.
 * @property {HTMLElement} tab             The slideout tab.
 * @property {function} openSlideOut       Opens the slideout.
 * @property {function} closeSlideOut      Closes the slideout.
 * @property {function} hideTab            Hides the slideout's tab.
 * @property {function} animateEntrance    Animates the first entrance of the slideout's tab.
 * @property {function} showCollapsedTab   Show the tab in its collapsed form.
 * @property {function} showExpandedTab    Show the tab in its expanded form.
 */

/**
 * Factory function that creates an instance of a Slideout. This instance
 * provide references to and can control the tab and main slideout display.
 *
 * @param {SlideoutFactoryOptions} options   An object of options to configure the slideout.
 * @returns {Slideout}
 */
window.slideoutFactory = function(options) {
    var html = document.querySelector("html");
    var whmSearchInput = document.getElementById("quickJump");
    var whmTopframeWrapper = document.getElementById("topFrameWrapper");

    // slideout specific variables
    var mainContainer = options.mainContainer;
    var tab = options.tab;
    var closeButton = options.closeButton;

    // Bail if we're missing elements. Don't throw so we don't break other slideouts
    if (!tab || !mainContainer || !closeButton) {
        return;
    }

    var slideout = {
        id: options.id,
        mainContainer: mainContainer,
        tab: tab,
        openSlideOut: openSlideOut,
        closeSlideOut: closeSlideOut,
        hideTab: hideTab,
        animateEntrance: animateEntrance,
        showCollapsedTab: showCollapsedTab,
        showExpandedTab: showExpandedTab,
    };
    window.CPANEL.slideoutList.add(slideout);

    // The tab and container elements are hidden by default, so make them visible.
    tab.classList.remove("hide");
    mainContainer.classList.remove("hide");

    /**
    * Expand the tab, wait for a predetermined time, and then collapse it.
    */
    function advertiseTab() {
        if(!tab && !mainContainer) {
            return;
        }

        tab.showExpanded();

        var animationCount = 0;
        var timedCollapse = function() {

            // Start the timer and remove the listener after both animations have finished.
            if(++animationCount >= 2) {
                setTimeout(function() {
                    if(!mainContainer.isActive()) {
                        tab.showCollapsed();
                    }
                }, 2500);
                tab.removeEventListener("animationend", timedCollapse);
            }
        };

        tab.addEventListener("animationend", timedCollapse);
    }

    /**
     * Animates the first entrance of the slideout's tab. Sometimes it will be
     * heavily animated (advertised) and sometimes it will be more subtle. This
     * is usually based on the cPanel session, as we don't want to barrage users
     * with animation on every page load and only want to show it upon their first
     * page load for the session.
     */
    function animateEntrance() {
        // The initial-state class ensures that there are no transition effects on first render.
        tab.classList.remove("initial-state");
        mainContainer.classList.remove("initial-state");

        if(canAdvertise()) {
            // set session state
            storeAdvertisedState();

            // Trigger the flashier animation
            advertiseTab();

        } else {
            // Trigger the subtle animation
            tab.showCollapsed();
        }
    }


    // Set up interactions that will persist
    if(mainContainer) {

        mainContainer.isActive = function() {
            return mainContainer.classList.contains("active");
        }

        // There is a listener at the document level that will close the slide-in on any
        // focus events, but we obviously don't want that to happen if they focus on anything
        // inside of the sidebar content area.
        mainContainer.addEventListener("focusin", function(e) {
            e.stopPropagation();
        });
    }

    if(closeButton) {
        closeButton.addEventListener("click", slideout.closeSlideOut);
    }

    if(tab) {
        tab.addEventListener("click", toggleSlideOut);
        tab.addEventListener("focus", function() {
            // Prevent the browser from shifting to show hidden contents when tabbing
            html.scrollLeft = 0;
            html.scrollRight = 0;
        });

        // Enable keyboard interactions with the tab
        tab.addEventListener("keyup", function(e) {
            if(
                normalizeKeyPress(e.key || e.keyCode) === " "
                || normalizeKeyPress(e.key || e.keyCode) === "Enter"
            ) {
                toggleSlideOut();
            }
        });

        /**
         * Shows the collapsed tab.
         */
        tab.showCollapsed = function() {
            tab.classList.add("tab-collapsed");
            tab.classList.remove("tab-hidden");
            tab.classList.remove("tab-expanded");
        };

        /**
         * Shows the expanded tab.
         */
        tab.showExpanded = function() {
            tab.classList.add("tab-expanded");
            tab.classList.remove("tab-hidden");
            tab.classList.remove("tab-collapsed");
        };

        /**
         * Hides the tab.
         */
        tab.hide = function() {
            tab.blur();
            tab.classList.add("tab-hidden");
            tab.classList.remove("tab-expanded");
            tab.classList.remove("tab-collapsed");
        };
    }

    /**
     * Shows the collapsed version of the tab.
     */
    function showCollapsedTab() {
        tab && tab.showCollapsed();
    }

    /**
     * Shows the expanded version of the tab.
     */
    function showExpandedTab() {
        tab && tab.showExpanded();
    }

    var debouncedRepositionSlideout = debounce(repositionSlideout, 100);

    /**
     * Calculates and sets the correct height for the slideout based on the
     * height of the WHM top bar.
     */
    function repositionSlideout() {
        if(whmTopframeWrapper){
            var height = whmTopframeWrapper.offsetHeight;
            var spacer = 1; // To introduce additional gap between top frame and slideout
            if(mainContainer){
                mainContainer.style.top = (height + spacer) + "px";
            }
        }
    }

    // Set up interactions that are added when the slide-out is open and
    // removed when it is closed
    var handlers = {
        /**
         * Closes the slideout upon certain key presses.
         */
        documentKeyup: function(e) {
            var key = normalizeKeyPress(e.key || e.keyCode);

            // Close the slide-out on ESC and WHM hotkeys
            if(mainContainer && mainContainer.isActive()) {
                if(
                    key === "Escape"
                    || key === "F2" && e.altKey
                    || key === "`"
                    || key === "/" && e.ctrlKey
                    || key === "/" && e.metaKey
                    || key === "*" && e.shiftKey
                ) {
                    slideout.closeSlideOut();
                }
            }
        },

        /**
         * Closes the slideout when the WHM search bar is focused.
         */
        whmSearchFocus: function() {
            if(mainContainer.isActive()) {
                slideout.closeSlideOut();
            }
        },

        /**
         * Recalculates WHM's top frame after a viewport resize.
         */
        windowResize: function() {
            debouncedRepositionSlideout();
        },

    };

    /**
     * Determines whether or not we should advertise the slideout tab or if it
     * should perform the more subdued intro animation.
     */
    function canAdvertise() {
        if (typeof(Storage) !== "undefined") {
            var cpSessionId = getCpanelSessionId();
            if(window.sessionStorage &&
                cpSessionId !== sessionStorage[options.sessionStorageKey]){
                return true;
            }
        }
        return false;
    }


    /**
     * Retrieves the cPanel-provided session ID.
     */
    function getCpanelSessionId() {
        if(window &&
            window.COMMON &&
            window.COMMON.securityToken){
                return window.COMMON.securityToken;
        }
        return null;
    }

    /**
     * Stores the cPanel-provided session ID into the browser's sessionStorage so
     * we can determine whether or not the slideout has already been advertised for
     * a given cPanel session.
     */
    function storeAdvertisedState() {
        if (typeof(Storage) !== "undefined") {
            // Incase session storage is not supported or is full
            try{
                sessionStorage[options.sessionStorageKey] = getCpanelSessionId();
            }catch(err){
                // Do nothing with exception
            }
        }
    }

    /**
     * Attaches listeners for events that represent the user attempting to
     * interact with WHM and trying to get out of the slideout. These are
     * only necessary when the slideout is open.
     */
    function attachListeners() {
        if(whmSearchInput) {
            whmSearchInput.addEventListener("focus", handlers.whmSearchFocus);
        }

        document.addEventListener("keyup", handlers.documentKeyup);
        document.addEventListener("focusin", handlers.documentFocus); // focusin bubbles

        // reposition slideout when document resizes
        window.addEventListener("resize", handlers.windowResize);
    }

    /**
     * Detaches listeners for events that represent the user attempting to
     * interact with WHM and trying to get out of the slide-out. These are
     * only necessary when the slideout is open.
     */
    function detachListeners() {
        if(whmSearchInput) {
            whmSearchInput.removeEventListener("focus", handlers.whmSearchFocus);
        }

        document.removeEventListener("keyup", handlers.documentKeyup);
        document.removeEventListener("focusin", handlers.documentFocus); // focusin bubbles
        window.removeEventListener("resize", handlers.windowResize);
    }

    var keyCodeMap = {
        13: "Enter",
        27: "Escape",
        32: " ", // Space
        56: "*",
        113: "F2",
        191: "/",
        192: "`",
    };

    /**
     * Accepts either a key (string) or keyCode (number) value from a KeyboardEvent object
     * and returns the corresponding key value. The keyCode property is deprecated, but offers
     * broader browser support, so we use it as a fallback while still using the simpler and
     * more future-proof KeyboardEvent.key strings in the code.
     *
     * @param {KeyboardEvent.key|KeyboardEvent.keyCode} keyCode   The value to normalize to a key value
     * @returns KeyboardEvent.key
     */
    function normalizeKeyPress(keyCode) {
        return (typeof keyCode === "number") ? keyCodeMap[keyCode] : keyCode;
    }

    /**
     * Opens the slideout and hides all slideout tabs. If an onOpen
     * callback was passed to the factory, it will be called here.
     */
    function openSlideOut() {
        mainContainer && mainContainer.classList.add("active");
        if(tab) {
            tab.hide();
            tab.setAttribute("aria-expanded", true);
        }


        // Set the position of the slideout based on whm top frame
        repositionSlideout();
        attachListeners();

        window.CPANEL.slideoutList.hideAllTabs();

        if(options.onOpen){
            options.onOpen();
        }
    }

    /**
     * Closes the slideout and shows all slideout tabs. If an onClose
     * callback was passed to the factory, it will be called here.
     */
    function closeSlideOut() {
        mainContainer && mainContainer.classList.remove("active");
        if(tab) {
            tab.showCollapsed();
            tab.setAttribute("aria-expanded", false);
        }
        detachListeners();

        window.CPANEL.slideoutList.showAllTabs();

        if(options.onClose){
            options.onClose();
        }
    }

    /**
     * Toggles the slideout, depending on its current state.
     */
    function toggleSlideOut() {
        if(mainContainer && mainContainer.isActive()) {
            slideout.closeSlideOut();
        }
        else {
            slideout.openSlideOut();
        }
    }

    /**
     * Hides the tab associated with this slideout.
     */
    function hideTab() {
        if(tab) {
            tab.hide();
        }
    }

    /**
     * Creates a new function that wraps a given function in a debouncer
     *
     * @param {function} func   The function to debounce
     * @param {number} delay    The debounce delay
     * @returns {function}   A new function that wraps the original in the debounce logic
     */
    function debounce(func, delay) {
        var inDebounce;
        return function() {
            var context = this;
            var args = arguments;
            clearTimeout(inDebounce)
            inDebounce = setTimeout(function(){
                func.apply(context, args)
                }, delay);
        }
    }

    return slideout;
}
Back to Directory File Manager