Viewing File: /usr/local/cpanel/whostmgr/docroot/templates/menu/main.js

/*
# cpanel - whostmgr/docroot/templates/menu/main.js Copyright 2024 cPanel, L.L.C.
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
*/

(function() {
    "use strict";

    var shared = (function() {
        return {
            setnvdata: function(key, value, store) {
                var data = {
                    "api.version": 1,
                    "personalization": {},
                };
                data["personalization"][key] = value;
                if (store) {
                    data["store"] = store;
                }

                var fetchPromise = fetch(PAGE.token + "/json-api/personalization_set", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify(data),
                });

                return fetchPromise.then(function(response) {
                    return response.json();
                });
            },
            debounce: function(callback, time) {
                var timer;

                return function() {
                    if (timer) {
                        clearTimeout(timer);
                    }
                    timer = setTimeout(function() {
                        callback();
                        timer = null;
                    }, time);
                };
            },
        };
    }());

    (function() {
        var groups = PAGE.expanded_groups || {};
        var localGroups = {};
        var saveExpandStatusDebounced = shared.debounce(saveExpandStatus, 500);

        /**
         * Checks cPanel version expiry
         *
         * @method checkVersionExpiry
         */
        function checkVersionExpiry() {
            var messageDiv = document.getElementById("versionExpiryMessage");
            var topSection = document.getElementById("topSection");
            if (messageDiv && topSection) {
                var request = new XMLHttpRequest();
                request.addEventListener("load", function(event) {
                    if (request.status >= 200 && request.status < 300 ||
                        request.status === 304) {
                        var response = JSON.parse(event.target.responseText);
                        if (response.data && response.data.expires_in_next_three_months) {
                            var currentVersionContent = document.getElementById("PLACEHOLDER_CURRENT_VERSION");
                            var expiryContent = document.getElementById("PLACEHOLDER_EXPIRY");
                            currentVersionContent.textContent = response.data.full_version;
                            expiryContent.textContent = new Date(response.data.expiration * 1000).toDateString();

                            messageDiv.classList.remove("hide");
                            topSection.classList.remove("hide");
                        }
                    }
                });
                request.open("GET", PAGE.token + "/json-api/get_current_lts_expiration_status?api.version=1", true);
                request.send();
            }
        }


        /**
         * Expands left navigation
         *
         * @method expandLeftNav
         */
        function expandLeftNav() {
            if (typeof window.commander !== "undefined" && window.commander) {
                window.commander.doNavExpandCollapse();
            }
        }

        function showHideAdditionalApps(info) {
            var additionaAppsDiv = document.getElementById("divAdditionalApps");
            if (info.detail === "expand") {
                additionaAppsDiv.classList.add("hide");
            } else {
                additionaAppsDiv.classList.remove("hide");
            }
        }

        /**
         * Hides important next steps
         *
         * @method dismissImportantNextSteps
         */
        function dismissImportantNextSteps() {
            var nextStepsContainer = document.getElementById("nextStepsSection");

            if (window["mixpanel"]) {
                window["mixpanel"].track("WHM-Important-Next-Steps-Dismiss-Clicked");
            }
            if (typeof nextStepsContainer !== "undefined" && nextStepsContainer) {
                nextStepsContainer.classList.add("hide-next-steps");
                shared.setnvdata("home:hide_important_next_steps", 1);
            }

        }

        /**
         * Hides alert from ISA
         *
         * @method dismissAlert
         */
        function dismissAlert() {
            var alertSection = document.getElementById("isaAlertSection");

            if (typeof alertSection !== "undefined" && alertSection) {
                alertSection.classList.add("hide");
            }
        }

        /**
         * Start the spinner on a change-view button.
         * @param {Event} e   The click event
         */
        function startChangeViewSpinner(e) {
            var spinnnerElem = this.querySelector(".fa-spin");
            if (spinnnerElem) {
                spinnnerElem.classList.remove("hide");
            }
        }

        /**
         * save expanded information to the server
         *
         * @method saveExpandStatus
         */
        function saveExpandStatus() {

            // Only send updated groups
            var changedGroups = Object
                .keys(localGroups)
                .reduce(function(_groups, group) {
                    if (groups[group] !== localGroups[group]) {
                        _groups[group] = localGroups[group];
                    }
                    return _groups;
                }, {});

            if (!Object.keys(changedGroups).length) {
                return;
            }
            groups = Object.assign(groups, localGroups);

            shared.setnvdata("toggle-status", groups, "home-tools-toggle-status");

            localGroups = {};
        }

        /**
         * Initilize collapsible group element
         *
         * @method collapsibleGroup
         * @param {Element} el - the root element of the collapsible group
         */
        function collapsibleGroup(el) {
            var expandButton = el.querySelector(".cp-group__header");
            expandButton.addEventListener("click", function() {
                var groupKey = el.getAttribute("data-group-key");
                var expanded = el.classList.contains("expanded");
                if (expanded) {
                    el.classList.remove("expanded");
                    localGroups[groupKey] = 0;
                } else {
                    el.classList.add("expanded");
                    localGroups[groupKey] = 1;
                }
                saveExpandStatusDebounced();
            });
        }

        function copyUuidHandler() {
            var uuidTxtEl = document.getElementById("txtAcctUuid");
            var copyMsgEl = document.getElementById("copyMsgContainer");
            const copyText = uuidTxtEl.textContent;

            navigator.clipboard
                .writeText(copyText)
                .then(() => {
                    copyMsgEl.classList.add("show-copy-success");
                    window.setTimeout(function() {
                        copyMsgEl.classList.remove("show-copy-success");
                    }, 3000);
                },
                (err) => {
                    console.error(err);
                });
        }

        /**
         * initialization
         *
         * @method init
         */
        function init() {
            checkVersionExpiry();

            var lnkDismissNextSetps = document.getElementById("lnkDismissNextSteps");
            if (typeof lnkDismissNextSetps !== "undefined" && lnkDismissNextSetps) {
                lnkDismissNextSetps.addEventListener("click", dismissImportantNextSteps);
            }

            var btnCloseAlert = document.getElementById("btnCloseAlert");
            if (typeof btnCloseAlert !== "undefined" && btnCloseAlert) {
                btnCloseAlert.addEventListener("click", dismissAlert);
            }

            var btnViewAllApps = document.getElementById("btnViewAllApps");

            if (typeof btnViewAllApps !== "undefined" && btnViewAllApps) {
                btnViewAllApps.addEventListener("click", expandLeftNav);
            }

            // These buttons are in a group and act as a set of radio buttons
            var topChangeViewButtons = document.querySelectorAll(".change-view-button");
            if (topChangeViewButtons.length) {
                Array.prototype.forEach.call(topChangeViewButtons, function(button) {
                    button.addEventListener("click", function(e) {
                        if (this.classList.contains("active")) {
                            e.preventDefault();
                            e.stopPropagation();
                            return;
                        }

                        // Deselect the other button as part of the radio functionality
                        var sibling = this.nextElementSibling || this.previousElementSibling;
                        if (sibling) {
                            sibling.classList.remove("active");
                        }

                        startChangeViewSpinner.call(this, e);
                        this.classList.add("active");
                    });
                });
            }

            Array.prototype.forEach.call(document.querySelectorAll(".cp-group"), collapsibleGroup);
            window.addEventListener("toggle-navigation", showHideAdditionalApps);

            // Account UUID copy event handler.
            var copyUuidLinkEl = document.getElementById("linkCopyUuid");
            var copyUuidIconEl = document.getElementById("iconCopyUuid");
            if (copyUuidLinkEl) {
                copyUuidLinkEl.addEventListener("click", copyUuidHandler);
            }
            if (copyUuidIconEl) {
                copyUuidIconEl.addEventListener("click", copyUuidHandler);
            }

            // Track links in Important Next Steps
            window.addEventListener("load", function() {

                // Ensure the section is present to avoid a warning, since it's not guaranteed to be present
                if (!document.querySelector("#important_next_steps")) {
                    return;
                }
                if (window["mixpanel"]) {
                    window["mixpanel"].track_links("#important_next_steps a", "WHM-Important-Next-Steps-Nav-Link", (linkEl) => {
                        var linkTitle = "";
                        const titleSelect = linkEl.querySelector(".cp-app__details-title");
                        if (titleSelect) {
                            linkTitle = titleSelect.innerHTML;
                        }
                        return { "nav-link-id": linkTitle };
                    });
                }
            });
        }

        init();

    }());

    (function() {
        var editMode = false;
        var ids = [];
        var sortable;

        var saveFavoritesDebounced = function(ids) {
            return shared.debounce(save(ids), 1000);
        };

        var saveSettingsDebounced = function(showFavoritesDescriptions) {
            return shared.debounce(saveSettings(showFavoritesDescriptions), 1000);
        };

        /**
         * Process all the components via the action.
         *
         * @param {string} kind - the tag name of the component
         * @param {function} action - the action to take on each component
         *
         * @returns Promise;
         */
        function allComponents(kind, action) {
            return customElements
                .whenDefined(kind)
                .then(function() {
                    var tag = kind;
                    var els = Array.from(document.querySelectorAll(tag));
                    els.forEach(function(el) {
                        action(el);
                    });
                });
        }

        /**
         * Process the one component via the action.
         *
         * @param {string} kind - the tag name of the component
         * @param {function} action - the action to take on each component
         * @returns
         */
        function oneComponent(kind, action) {
            return customElements
                .whenDefined(kind)
                .then(function() {
                    var tag = kind;
                    var el = document.querySelector(tag);
                    action(el);
                });
        }

        /**
         * Adjust the hidden state of the passed in element based on the editMode
         *
         * @param {Boolean} editMode
         * @param {HtmlElement} el
         */
        function setHiddenClass(editMode, el) {
            if (editMode) {
                if (el.classList.contains("hidden")) {
                    el.classList.remove("hidden");
                }
            } else {
                if (!el.classList.contains("hidden")) {
                    el.classList.add("hidden");
                }
            }
        }

        /**
         * Event handler for toggling the tools edit mode
         *
         * @param {Event} e
         */
        function toggleEditMode(e) {
            editMode = !editMode;

            setHiddenClass(!editMode && ids.length === 0, getPlaceholder());

            // Update the cards already in the list
            allComponents("cp-favorite", function(el) {
                el.setEditMode(editMode);
            });

            allComponents("cp-favorite-selector", function(el) {
                el.setEditMode(editMode);
            });

            allComponents("cp-app", function(el) {
                el.setEditMode(editMode);
            });

            var btnEl = document.getElementsByClassName("toggle-edit-favorites-button");
            if (btnEl && btnEl.length) {
                var txtEditOnEl = btnEl[0].querySelector(".toggle-edit-favorites-button__select");
                if (txtEditOnEl) {
                    setHiddenClass(!editMode, txtEditOnEl);
                }

                var txtEditOffEl = btnEl[0].querySelector(".toggle-edit-favorites-button__done");
                if (txtEditOffEl) {
                    setHiddenClass(editMode, txtEditOffEl);
                }
            }

            var instructionEl = document.getElementsByClassName("cp-favorite-instructions");
            if (instructionEl) {
                setHiddenClass(editMode, instructionEl[0]);
            }

            sortable.option("disabled", !editMode);
        }

        /**
         * Helper to persist the personalized tools
         *
         * @param {string[]} ids - The unique identifiers for the tools.
         */
        function save(ids) {
            return shared.setnvdata("favorites", ids);
        }

        /**
         * Preserve the users selection for whether to show or hide the additional descriptions
         * in the Favorites section.
         *
         * @param {boolean} showFavoritesDescriptions - show/hide the descriptions in the favorites
         * @returns
         */
        function saveSettings(showFavoritesDescriptions) {
            return shared.setnvdata("showFavoritesDescriptions", showFavoritesDescriptions);
        }

        /**
         * Find the tool data for the application.
         *
         * @param {string} groupId
         * @param {string} appId
         * @returns Application
         */
        function findTool(groupId, appId) {
            if (groupId === "plugins") {
                var plugin = PAGE.plugins.find(function(app) {
                    return app.key === appId;
                });
                if (plugin) {
                    return plugin;
                }
            }

            var group = PAGE.tools.groups.find(function(group) {
                return group.key === groupId;
            });

            if (!group) {
                return;
            }

            return group.items.find(function(app) {
                return app.key === appId;
            });
        }

        /**
         * The placeholder is used when there are no favorites selected
         * by the user to remind them that there is a cool feature they are
         * ignoring.
         */
        var placeholderEl;

        /**
         * Retrieve the DOM element for the tools placeholder text.
         */
        function getPlaceholder() {
            if (!placeholderEl) {
                placeholderEl = document.getElementsByClassName("tools-grid-placeholder")[0];
            }
            return placeholderEl;
        }

        /**
         * Show the placeholder
         */
        function showPlaceholder() {
            setHiddenClass(true, getPlaceholder());
        }

        /**
         * Hide the placeholder
         */
        function hidePlaceholder() {
            setHiddenClass(false, getPlaceholder());
        }

        /**
         * Add a new favorite.
         *
         * @param {string} id - the unique identifier for the favorite
         */
        function addItemToFavorites(id) {

            // Find the application
            var parts = id.split("$");
            var application = findTool(parts[0], parts[1]);
            if (!application) {
                return;
            }

            hidePlaceholder();

            // Add the item to the view
            PAGE.favorites.push({
                group: parts[0],
                key: parts[1],
                url: application.fullUrl,
                target: application.target || "_self",
                iconUrl: application.iconUrl,
                name: application.itemdesc,
                description: application.description,
            });
            oneComponent("cp-favorite-list", function(el) {

                // need a new reference for stenciljs to
                // recognize the data changed
                el.favorites = PAGE.favorites.slice();
            });

            // Persist the data
            var newIds = Array.from(ids);
            newIds.push(id);
            saveFavoritesDebounced(newIds);
            ids = newIds;
        }

        /**
         * Remove the item from the
         *
         * @param {string} uniqueId - unique identifer for the favorite
         */
        function removeItemFromFavorites(uniqueId) {

            // Remove the item from the view.
            PAGE.favorites = PAGE.favorites.filter(function(favorite) {
                return uniqueId !== favorite.group + "$" + favorite.key;
            });
            oneComponent("cp-favorite-list", function(el) {

                // need a new reference for stenciljs to
                // recognize the data changed
                el.favorites = PAGE.favorites.slice();
            });

            // Unselect the cp-favorite-selector component
            var appName = uniqueId.split("$")[1];
            var el = document.querySelector("cp-favorite-selector[name=" + CSS.escape(appName) + "]");
            if (el) {
                el.selected(false);
            }

            // Persist the data.
            var newIds = ids.filter(function(id) {
                return id !== uniqueId;
            });

            saveFavoritesDebounced(newIds);
            ids = newIds;
        }

        /**
         * Initialize the toggle button handling
         */
        function initializeToggleButton() {
            var btnEl = document.getElementsByClassName("toggle-edit-favorites-button");
            if (btnEl && btnEl.length) {
                btnEl[0].addEventListener("click", toggleEditMode);
            }
        }

        /**
         * Creates a callback function for the close (x) links on each favorite.
         *
         * @param {HtmlElement} el
         */
        function removeFavoriteFactory(el) {
            return function(e) {
                var toolEl = el;
                var removeId = toolEl.group + "$" + toolEl.name;

                removeItemFromFavorites(removeId);

                allComponents("cp-main-menu", function(el) {
                    el.updateFavorites(PAGE.favorites.slice());
                });
            };
        }

        /**
         * Initialize the favorites handling
         */
        function initializeFavorites() {
            allComponents("cp-favorite", function(el) {
                el.setEditMode(editMode);
                if (el.removeFavoriteHandler) {
                    return;
                }

                el.removeFavoriteHandler = removeFavoriteFactory(el);
                el.addEventListener("removeFavorite", el.removeFavoriteHandler);
            });
        }

        /**
         * Initialize the favorite selectors
         */
        function initializeSelectors() {

            // eslint-disable-next-line no-undef
            var initialSet = new Set(
                PAGE.favorites.map(function(favorite) {
                    return favorite.group + "$" + favorite.key;
                })
            );

            allComponents("cp-favorite-selector", function(el) {
                el.uniqueId().then(function(identifier) {

                    var id = identifier.toString();
                    if (initialSet.has(id)) {
                        el.selected(true);
                    }

                    return el;
                }).then(function(el) {
                    el.addEventListener("changeFavorite", function(e) {
                        var id = e.detail.toString();
                        if ( e.detail.selected ) {
                            addItemToFavorites(id);
                        } else {
                            removeItemFromFavorites(id);
                        }

                        allComponents("cp-main-menu", function(el) {
                            el.updateFavorites(PAGE.favorites.slice());
                        });
                    });
                });

            });
        }

        /**
         * Wait for a global variable to appear as in indication that the supporting
         * library is now loaded. When its present call the callback that depends on
         * this libraries presence.
         *
         * @param {string} name - the name of the global we are waiting for.
         * @param {Function} fn - the function that depends on the library.
         */
        function whenApiAvailable(name, fn) {
            var interval = 2; // ms
            window.setTimeout(function() {
                if (window[name]) {
                    fn(window[name]);
                } else {
                    whenApiAvailable(name, fn);
                }
            }, interval);
        }

        /**
         * Move an element from one position in the array to another
         * The general idea for this algorigthm was takend from:
         *   https://github.com/granteagon/move/blob/master/src/index.js
         * which is MIT licensed.
         *
         * @param {Array} array - the array to minipulate
         * @param {number} from - the index of the source element.
         * @param {number} to - the index of the destination element.
         * @returns {Array} - the new array.
         */
        function move(array, from, to) {
            var length = array.length;
            var delta = from - to;
            var newArray;

            if (delta > 0) {

                // Move to the left
                newArray = array.slice(0, to);
                newArray.push(array[from]);
                array.slice(to, from).forEach(function(item) {
                    newArray.push(item);
                });
                array.slice(from + 1, length).forEach(function(item) {
                    newArray.push(item);
                });

                return newArray;
            } else if (delta < 0) {

                // Move to the right
                to += 1;

                newArray = array.slice(0, from);
                array.slice(from + 1, to).forEach(function(item) {
                    newArray.push(item);
                });
                newArray.push(array[from]);
                array.slice(to, length).forEach(function(item) {
                    newArray.push(item);
                });
                return newArray;
            }

            // No move
            return array;
        }

        /**
         * Initialize the drag drop sorting of the favorites.
         */
        function initializeSortable() {
            whenApiAvailable("Sortable", function() {
                var list = document.getElementsByClassName("favorites-container")[0];
                sortable = Sortable.create(list, { // eslint-disable-line no-undef
                    disabled: true,
                    animation: 100,
                    delay: 200,
                    delayOnTouchOnly: true,
                    onEnd: function(/** Event*/evt) {
                        var from = evt.oldIndex;
                        var to = evt.newIndex;

                        // Persist the data
                        var newIds = move(ids, from, to);
                        PAGE.favorites = move(PAGE.favorites, from, to);
                        saveFavoritesDebounced(newIds);
                        ids = newIds;
                        allComponents("cp-main-menu", function(el) {
                            el.updateFavorites(PAGE.favorites.slice());
                        });
                    },
                });
            });
        }

        /**
         * Build the ids list
         */
        function initializeIds() {
            ids = PAGE.favorites.reduce(function(acc, current) {
                acc.push(current.group + "$" + current.key);
                return acc;
            }, []);
        }

        /**
         * Wire in a temporary UI to test.
         */
        function initializeEditOptions(chkEl) {
            if (chkEl) {
                chkEl.addEventListener("toggle", function(e) {
                    var showDescriptions = e.detail.state === "on";
                    saveSettingsDebounced(showDescriptions);
                    allComponents("cp-favorite-list", function(el) {
                        el.updateOptions({ showDescriptions: showDescriptions });
                    });
                });
            }
        }

        // Initialize the view
        initializeIds();
        oneComponent("cp-favorite-list", function(el) {
            el.favorites = PAGE.favorites.slice();
        });
        oneComponent("cp-favorite-list", function(el) {
            el.addEventListener("favoritesLoaded", initializeFavorites);
        });
        oneComponent("cpw-toggle-switch", function(el) {
            initializeEditOptions(el);
        });
        initializeSelectors();
        initializeToggleButton();
        initializeSortable();
    }());
}());
Back to Directory File Manager