Viewing File: /usr/local/cpanel/share/libraries/cjt2/src/directives/alertList.js

/*
# cjt/directives/alertList.js                        Copyright 2022 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
*/

/* global define: false */

define(
    [
        "angular",
        "jquery", // Used for the height/width methods
        "cjt/core",
        "lodash",
        "ngAnimate",
        "ngSanitize",
        "cjt/directives/alert",
        "cjt/config/componentConfiguration",
        "cjt/services/alertService",
        "cjt/templates" // NOTE: Pre-load the template cache
    ],
    function(angular, $, CJT, _) {
        "use strict";

        var DEFAULT_INLINE = false;

        var RELATIVE_PATH = "libraries/cjt2/directives/alertList.phtml";

        var module = angular.module("cjt2.directives.alertList", [
            "cjt2.config.componentConfiguration",
            "cjt2.templates",
            "ngAnimate",
            "ngSanitize",
            "cjt2.directives.alert",
        ]);

        /**
         * We don't want to include the top bar in our calculations of available space
         * for the alertList content. These values are duplicated in alertList.less, so
         * please update them in both places if they ever change.
         */
        var TOP_BAR_OFFSETS = {

            whostmgrSm: 120,
            cpanelSm: 52,
            webmailSm: 52,

            whostmgrXs: 70,
            cpanelXs: 30,
            webmailXs: 30,
        };

        /**
         * Validate a position.
         *
         * @method _validatePosition
         * @param  {String}  position A potential value.
         * @return {Boolean} true if valid, false otherwise.
         */
        function _validatePosition(position) {
            if (!position) {
                return true;
            } // It's ok for it not to be set
            switch (position) {
                case "top-left":
                case "top-middle":
                case "top-right":
                case "bottom-left":
                case "bottom-middle":
                case "bottom-right":
                case "middle-left":
                case "middle-middle":
                case "middle-right":
                    return true;
                default:
                    window.console.log("Invalid alertList.position set. It must be one of: top-left, top-middle, top-right, bottom-left, bottom-middle, bottom-right, middle-left, middle-middle, middle-right");
                    return false;
            }
        }

        /**
         * This is a directive that creates a list of alert directives using the alertService.
         *
         * Basic usage in a template:
         * <cp-alert-list></cp-alert-list>
         *
         * Template usage with a non-default alert group and auto-close:
         * <cp-alert-list alert-group="'testGroup'" auto-close="2000"></cp-alert-list>
         *
         * Please note that quotes are required in the alert-group attribute when
         * using a string.
         *
         * Now to add an alert, you can do something like any of the following:
         * alertService.add({
         *     message: "This is my alert message that is not closeable",
         *     closeable: false
         * });
         *
         * alertService.add({
         *     message: "This alert will stack with any alerts already present",
         *     replace: false
         * });
         *
         * alertService.add({
         *     message: "This alert specifies the type instead of using the default",
         *     type: "danger"
         * });
         *
         * alertService.add({
         *     message: "This alert add to the specified group instead of the default",
         *     type: "info",
         *     group: "testGroup"
         * });
         *
         * Please see the alertService documentation for more information.
         */
        module.directive("cpAlertList", [
            "alertService",
            "componentConfiguration",
            function(
                alertService,
                componentConfiguration
            ) {

                return {
                    restrict: "E",
                    templateUrl: CJT.config.debug ? CJT.buildFullPath(RELATIVE_PATH) : RELATIVE_PATH,
                    replace: true,
                    scope: {
                        alertGroup: "=",
                        autoClose: "=",
                        position: "=",
                    },
                    controller: ["$scope", "$element", "$window", "$attrs",
                        function($scope, $element, $window, $attrs) {

                            $scope.rules = {
                                position: null,
                                inline: DEFAULT_INLINE
                            };

                            $scope.$watch("inline", function() {
                                if ($scope.rules.inline !== $scope.inline) {
                                    refreshPositionRules();
                                }
                            });

                            $scope.$watch("position", function() {
                                if ($scope.rules.position !== $scope.position && _validatePosition($scope.position)) {
                                    refreshPositionRules();
                                }
                            });

                            $scope.$watchCollection(function() {
                                return componentConfiguration.getComponent("alertList");
                            }, function() {
                                refreshPositionRules();
                            });

                            // NOTE: Due to the dynamic nature of the whm header with breadcrumbs
                            // causing the header to grow in an unpredictable way, we have to
                            // dynamically adjust the top from the static value defined above.
                            // These expensive adjustments are only needed right now for whm.
                            // All code that uses them will check if the contentContainer is
                            // defined to make the decisions about applying the expensive extra
                            // adjustments.
                            //
                            // Other solutions I looked at:
                            //   * ResizeObserver - not native except for Chrome
                            //   * MutationObserver - complex to implement
                            //   * ResizeObserverPolyfill - ok, but requires adding an RPM, requires inefficent timers for some browsers.
                            //
                            // The current solution just addresses the problem where it exists
                            // presently without adding a polyfill or providing other signifigant
                            // cross-browser complex solutions at least for the size testing.

                            var contentContainer = CJT.isWhm() ? angular.element("#contentContainer") : null;
                            var contentContainerTop = getDefaultContainerTop();

                            /**
                             * Fetch the default top position. This is the hardcoded minimum.
                             *
                             * @return {Number} Pixel location to set as the top based only
                             * on the static minimums defined by them and grid size.
                             */
                            function getDefaultContainerTop() {
                                var customApplicationWidth = CJT.isWebmail() ? 667 : 768;

                                // We need a default even for whm or the fixed top will get set to 0 which will
                                // trigger the hidden top of the application problem.
                                return $window.innerWidth < customApplicationWidth ?
                                    TOP_BAR_OFFSETS[CJT.applicationName + "Xs"] :
                                    TOP_BAR_OFFSETS[CJT.applicationName + "Sm"];
                            }

                            /**
                             * Update contentContainerTop with the actual top of the contentContainer
                             * in WHM, in case the contentContainer has been pushed down below its default
                             * top. For other environments, we will just use the defaults, since their
                             * contents will always start at the default position for a given width.
                             *
                             * In either case, this method has the side effect of setting the alertList's
                             * top property if it has changed.
                             *
                             * @method updateContentContainerTop
                             */
                            function updateContentContainerTop() {
                                var defaultTopForCurrentWidth = getDefaultContainerTop();
                                if (contentContainer && contentContainer.length) {

                                    // Handle WHM
                                    var actualContainerTop = contentContainer[0].getBoundingClientRect().top;
                                    if (actualContainerTop !== contentContainerTop) {
                                        contentContainerTop = Math.max(actualContainerTop, defaultTopForCurrentWidth);
                                        $element.css("top", contentContainerTop);
                                    }
                                } else if (contentContainerTop !== defaultTopForCurrentWidth) {

                                    // Handle everything else, if the default top has changed
                                    contentContainerTop = defaultTopForCurrentWidth;
                                    $element.css("top", contentContainerTop);
                                }
                            }

                            /**
                             * Calculate the height at which to turn on the scrollbar
                             * for the alert list.
                             *
                             * @method calculateHeightToTurnOnScroll
                             */
                            function calculateHeightToTurnOnScroll() {
                                var windowHeight = $window.innerHeight;
                                $scope.heightToTurnOnScroll = windowHeight - contentContainerTop;
                            }

                            /**
                             * Update the position properties
                             *
                             * @method updatePosition
                             * @param  {String}  position Position of the alertList if inline is false.
                             * @param  {Boolean} inline   Inline if true, positioned otherwise.
                             */
                            function updatePosition(position, inline) {
                                if (position !== null) {
                                    $scope.rules.position = position;
                                }
                                if (inline !== null) {
                                    $scope.rules.inline = inline;
                                }
                            }

                            /**
                             * Refresh the positioning rules from the attributes and
                             * the componentConfiguration service.
                             *
                             * @method refreshPositionRules
                             */
                            function refreshPositionRules() {
                                var rules = componentConfiguration.getComponent("alertList");
                                updatePosition(rules.position, rules.inline);

                                if ($attrs.hasOwnProperty("inline")) {
                                    updatePosition(null, true);
                                }

                                if (angular.isDefined($scope.position) && $scope.position && _validatePosition($scope.position)) {
                                    updatePosition($scope.position, null);
                                }
                            }

                            /**
                             * Handle resize to adjust the heightToTurnOnScroll
                             * scope variable.
                             *
                             * @method  onResize
                             * @private
                             */
                            function onResize() {

                                updateContentContainerTop();
                                calculateHeightToTurnOnScroll();

                                // manual $digest required as resize event
                                // is outside of angular
                                $scope.$digest();
                            }

                            /**
                             * This method allows the user to close the alert only when it appears
                             * in the center of the page, by pressing escape key or by clicking
                             * anywhere but the alert popup.
                             *
                             * @method closeAlerts
                             * @param {Event} event - The keyboard/mouse event.
                             */
                            function closeAlerts(event) {
                                if ($scope.rules.inline || $scope.rules.position !== "middle-middle" || !$scope.alertsPresent) {
                                    return;
                                }

                                // close all alerts when the ESC key is pressed or when a click is caught outside of the alert itself
                                // we use closest here to ensure that we are not clicking inside of the alert container
                                if ((event.type === "keyup" && event.keyCode === 27) ||
                                    (event.type === "click" && angular.element(event.target).closest(".alert-container").length === 0)) {
                                    alertService.clear(null, $scope.alertGroup);

                                    // Unregister the click & key events.
                                    angular.element($window).off("keyup click", closeAlerts);
                                    $scope.$digest();
                                }
                            }

                            var debounceOnResizeFn = _.throttle(onResize, 60);
                            angular.element($window)
                                .on("resize", debounceOnResizeFn)
                                .on("toggle-navigation", onResize);

                            $scope.$on("$destroy", function() {
                                angular.element($window)
                                    .off("resize", debounceOnResizeFn)
                                    .off("toggle-navigation", onResize)
                                    .off("keyup click", closeAlerts);
                            });

                            // Get the initial UI height.
                            updateContentContainerTop();
                            calculateHeightToTurnOnScroll();
                            refreshPositionRules();

                            /**
                             * Gets the position classes to apply to the list container based on the
                             * user's settings in nvdata:alert-list-rules. If this is an inline list,
                             * then we don't provide the user's preference. This should only be used
                             * in special cases where local feedback is required.
                             *
                             * @method getPositionClasses
                             * @scope
                             * @return {String} css classname list to apply to the alertList class attribute.
                             */
                            $scope.getPositionClasses = function() {
                                return "position-" + ($scope.rules.inline ? "inline" : $scope.rules.position);
                            };

                            /**
                             * Determines whether or not the alert list has exceeded the viewable
                             * height of the page.
                             *
                             * @return {Boolean}   True if the list of alerts is taller than the
                             *                     visible space below the top bar.
                             */
                            $scope.needsScrollbar = function() {
                                if ($scope.rules.inline) {
                                    return false;
                                }

                                if (!angular.isDefined($scope.heightToTurnOnScroll)) {
                                    calculateHeightToTurnOnScroll();
                                }

                                var listHeight = $element.find(".alert-list").height();
                                return ($scope.alerts && $scope.alerts.length > 0 && listHeight >= $scope.heightToTurnOnScroll);
                            };

                            /**
                             * Set the height of the directive container equal to the visible space
                             * below the top bar if we need a scrollbar or the height has changed.
                             */
                            $scope.$watchGroup([
                                "needsScrollbar()",
                                "heightToTurnOnScroll",
                            ], function(newVals) {
                                var needsScrollbar = newVals[0];
                                var heightToTurnOnScroll = newVals[1];

                                if (needsScrollbar) {
                                    $element.css("height", heightToTurnOnScroll + "px");
                                } else {
                                    $element.css("height", "auto");
                                }
                            });

                            // Bind to the alert array from the service
                            $scope.alerts = alertService.getAlerts($scope.alertGroup);

                            $scope.$watchCollection("alerts", function(alerts) {
                                var applyAutoClose = !CJT.isE2E() && $scope.autoClose;
                                if (alerts.length) {
                                    $scope.alertsPresent = true;
                                } else {
                                    $scope.alertsPresent = false;
                                }
                                alerts.forEach(function(alert) {

                                    // If an autoClose is provided add it to any of the alerts
                                    // that don't define their own auto close.
                                    // NOTE: If we are running in an e2e test, we want to disable
                                    // auto-close.
                                    if (applyAutoClose && !alert.autoClose) {
                                        alert.autoClose = $scope.autoClose;
                                    }

                                    /**
                                     * Add the closable property as true to any non-inline alert
                                     * since these were not originally designed to cover up stuff
                                     * and will now. This means that far less of the code needs to
                                     * be modified to make these alertList changes work.
                                     */
                                    if (!$scope.rules.inline) {
                                        alert.closeable = true;
                                    }
                                });
                            });

                            /**
                             * Event addAlertCalled used to register
                             * click/keypup events for center positioned
                             * alerts.
                            */
                            $scope.$on("addAlertCalled", function(event) {
                                angular.element($window).on("keyup click", closeAlerts);
                            });

                            /**
                             * Close the alert
                             *
                             * @method closeAlert
                             * @param  {Number} index Position in the list to close.
                             */
                            $scope.$on("closeAlertCalled", function(event, args) {
                                alertService.removeById(args.id, $scope.alertGroup);
                                angular.element($window).off("keyup click", closeAlerts);
                            });
                        }
                    ]
                };

            }
        ]);

        module.animation(".alert-container", ["$animateCss", function($animateCss) {
            return {
                enter: function(elem, done) {
                    var height = elem[0].offsetHeight;
                    return $animateCss(elem, {
                        from: { height: "0" },
                        to: { height: height + "px" },
                        duration: 0.3,
                        easing: "ease-out",
                        event: "enter",
                        structural: true
                    })
                        .start()
                        .finally(function() {
                            elem[0].style.height = "";
                            done();
                        });
                },
                leave: function(elem, done) {
                    var height = elem[0].offsetHeight;
                    return $animateCss(elem, {
                        event: "leave",
                        structural: true,
                        from: { opacity: "1" },
                        to: { opacity: "0", transform: "translateX(50px)" },
                        duration: 0.3,
                        easing: "ease-out",
                    })
                        .start()
                        .done(function() {
                            $animateCss(elem, {
                                event: "leave",
                                structural: true,
                                from: { height: height + "px" },
                                to: { height: "0" },
                                duration: 0.3,
                                easing: "ease-out",
                            }).start().finally(function() {
                                done();
                            });
                        });
                },
            };
        }]);
    }
);
Back to Directory File Manager