view src/DML/MainVisBundle/Resources/assets/marionette/modules/NotificationsModule.js @ 1:f38015048f48 tip

Added GPL
author Daniel Wolff
date Sat, 13 Feb 2016 20:43:38 +0100
parents 493bcb69166c
children
line wrap: on
line source
"use strict";
/**
 * Notification options:
 *
 *   id
 *      null or string
 *      if the same as before, the old notification with the same id is replaced with the current one
 *
 *   modifiers
 *      [string1, string2]
 *      bem modifiers (define look and feel)
 *      availabilities:
 *          no-padding
 *          type_warning
 *          type_error
 *          ttl_ever
 *          ttl_10
 *          ttl_11
 *          ttl_20
 *          ...
 *
 *   content
 *      text / jquery object
 *      what to show in the notification
 *
 *   onClose
 *      function(options)
 *      what to do when the notification is manually closed
 *
 *   onExpire
 *      function(options)
 *      what to do when the notification is removed automatically
 *
 *   keepContentInMemoryAfterRemoval
 *      does not destroy the content on notification removal (it retains DOM nodes and event listeners, which can be reused later)
 *
 */
App.module("NotificationsModule", function(NotificationsModule, App, Backbone, Marionette, $, _, Logger) {
    // Prevent auto start
    NotificationsModule.startWithParent = false;

    // Define options
    var defaultModuleOptions = {
        defaultTTL: 10, // time to live
        minTTLOnUnfreeze: 6 // time to live after the notification is unfrozen (mouse went out)
    };
    var moduleOptions;

    // Define private variables
    var logger = null;
    var $notifications = null;
    var $notificationStack = null;

    // Define private methods
    var addNewNotification = null;
    var removeNotification = null;
    var updateNotification = null;

    var notificationTimeoutHandler = null;

    /*
     * Structure of "$notification" object
     * .notification_body
     *      .notification_content
     *      .notification_closer
     * data
     *    options
     *      ...
     */

    /**
     * Initialization checker
     */
    var assertModuleIsInitialized = function() {
        if (!$notifications) {
            throw "NotificationsModule has not been initialized";
        }
    };

    /**
     * Module initializer
     *
     */
    NotificationsModule.addInitializer(function(options){
        moduleOptions = _.extend(defaultModuleOptions, options);

        logger = Logger.get("NotificationsModule");
        //logger.setLevel(Logger.DEBUG);

        logger.debug("Begin init");

        $notifications = $(".app__notifications")
            .addClass("notifications");

        $notificationStack = $.bem.generateElement("notifications", "stack")
            .appendTo($notifications);

        // Sets up the internals of the notification
        var setupNotification = function($notification, notificationOptions) {
            // Content
            var $notificationContent = $notification.children().eq(0).children().eq(0);
            if (!$notificationContent.hasClass("notification__content")) {
                $notificationContent = $notificationContent.children().eq(0);
            }
            $notificationContent.children().detach();
            if (notificationOptions.content instanceof $) {
                $notificationContent.append(notificationOptions.content);
            } else {
                $notificationContent.html(notificationOptions.content);
            }

            $notification.attr("data-id", notificationOptions.id);
            $notification.data("options", notificationOptions);
            $notification.data("$content", $notificationContent);

            // Update css modifiers
            var modifiersAsArray = null;
            if (_.isString(notificationOptions.modifiers)) {
                modifiersAsArray = notificationOptions.modifiers.split(" ");
            }
            if (_.isArray(notificationOptions.modifiers)){
                modifiersAsArray = notificationOptions.modifiers;
            }
            var state = $notification.getMod("notification", "state");
            $notification.attr("class", "notification notification_state_" + state);
            if (_.isArray(modifiersAsArray) && modifiersAsArray.length) {
                $notification.addClass("notification_" + modifiersAsArray.join(" notification_"));
            }

            // Set ttl (time to live) if needed
            if (!$notification.getMod("notification", "ttl")) {
                $notification.setMod("notification", "ttl", moduleOptions.defaultTTL);
            }
        };

        var freezeTTL = function () {
            logger.debug("Freeze ttl", this);
            $(this).addClass("notification_ttl-frozen");
        };
        var unfreezeTTL = function () {
            logger.debug("Unfreeze ttl", this);
            var $this = $(this);
            var ttl = $this.getMod("notification", "ttl");
            if (ttl * 1 == ttl && ttl < moduleOptions.minTTLOnUnfreeze) {
                $this.setMod("notification", "ttl", moduleOptions.minTTLOnUnfreeze);
            }
            $this.removeClass("notification_ttl-frozen");
        };
        var stopTTL = function () {
            logger.debug("Stop ttl", this);
            $(this).setMod("notification", "ttl", "ever");
        };

        // A function that adds a new notification
        addNewNotification = function(notificationOptions) {
            logger.info("Adding new notificaton", notificationOptions);

            var $notificationContent = $.bem.generateElement("notification", "content");
            var $notificationCloser  = $.bem.generateElement("notification", "closer");
            var $notificationBody = $.bem.generateElement("notification", "body")
                .append($notificationContent, $notificationCloser);

            var $notification = $.bem.generateBlock("notification")
                .append($notificationBody);

            var closeFunction = function() {
                removeNotification($notification);
                var onClose = notificationOptions.onClose;
                if (_.isFunction(onClose)) {
                    try {
                        onClose($notification);
                    } catch (e) {
                        logger.error("Error when handling onClose", e, $notification, onClose);
                    }
                }
            };
            $notificationCloser.click(closeFunction);
            $notification.mouseover(freezeTTL);
            $notification.mouseout(unfreezeTTL);
            $notification.click(stopTTL);

            setupNotification($notification, notificationOptions);
            $notification.setMod("notification", "state", "pre-shown");
            $notification.appendTo($notificationStack);

            var notificationBodyHeight = $notificationBody.outerHeight();
            $notification.height(notificationBodyHeight);
            $notification.hide();
            $notificationBody.hide();

            // First show the container, and then slide the body
            $notification.show("blind", {
                direction: "up",
                //easing: "easeInOutQuart"

            }, $notificationStack.children().length == 1 ? 1 : 100 + notificationBodyHeight / 10 * 30, function() {
                $notificationBody.show("slide", {
                    direction: "right",
                    easing: "easeOutQuart"
                }, 500, function() {
                    $notification.setMod("notification", "state", "shown");
                });
            });
        };

        // A function that replaces the contents of the given notification
        updateNotification = function($notification, notificationOptions) {
            logger.info("Updating notification", $notification, "with new options", notificationOptions);

            if ($notification.length !== 1) {
                logger.error(_.str.sprintf("Can’t replace a notification, this can be done only when $notification contains a single instance.", $notification));
                return;
            }

            // Set up the notification again
            setupNotification($notification, notificationOptions);

            // Pay user's attention to this notification
            if (!$notification.is(":animated") && $notification.hasClass("notification_state_shown")) {
                $notification.effect("shake", {
                    direction: "up",
                    distance: 5,
                    times: 2
                });
            }
        };

        // Removes the notification (hides it and removes)
        removeNotification = function($notification) {
            logger.debug("Begin remove notification", $notification);
            if ($notification.hasMod("notification", "state_pre-hidden")) {
                logger.debug("Notification", $notification, "is already being hidden by another handler");
                return;
            };
            $notification.setMod("notification", "state", "pre-hidden");

            // First fade the body and then blind the container
            var $notificationBody = $notification.children().eq(0);
            var notificationBodyHeight = $notificationBody.outerHeight();
            $notification.children().eq(0).fadeOut(
                200,
                function() {
                    $notification.hide("blind", {
                        direction: "up",
                    }, 100 + notificationBodyHeight / 10 * 30, function() {
                        if ($notification.data("options").keepContentInMemoryAfterRemoval) {
                            $notification.data("$content").detach();
                        }
                        $notification.remove();
                    });
            });

        };

        // Every second decrease ttl (time to live) for each notification
        notificationTimeoutHandler = setInterval(function() {
            logger.debug("Notification lifetime interval triggered");
            $notificationStack
                .children()
                .filter(".notification_state_shown")
                .not(".notification_ttl-frozen") // this class is set to notification when it is hovered over with a mouse
                .each(function() {
                    var $thisNotification = $(this);
                    var ttl = $thisNotification.getMod("notification", "ttl");
                    if (ttl * 1 != ttl) {
                        return;
                    }
                    ttl = ttl - 1;
                    if (ttl < 0) {
                        removeNotification($thisNotification);
                        var onExpire = $thisNotification.data("options").onExpire;
                        if (_.isFunction(onExpire)) {
                            try {
                                onExpire($thisNotification);
                            } catch (e) {
                                logger.error("Error when handling onExpire", e, $thisNotification, onExpire);
                            }
                        }

                    } else {
                        $thisNotification.setMod("notification", "ttl", ttl);
                    }
                });
        }, 1000);

        logger.debug("End init");
    });

    /**
     * Show a notification
     */
    NotificationsModule.show = function(notificationOptions) {
        assertModuleIsInitialized();
        logger.debug("Begin showing a new notification", notificationOptions);

        var extendedNotificationOptions = _.extend({
            id: null,
            modifiers: null,
            content: "",
            onClose: null,
            onExpire: null,
            keepContentInMemoryAfterRemoval: false
        }, notificationOptions);

        // Attempt to find and update notification with the same id (if it has been defined)
        // Or to create a new notification
        if (extendedNotificationOptions.id) {
            var $notificationWithTheSameId = $notificationStack.find(_.str.sprintf(".notification[data-id='%s']", extendedNotificationOptions.id)).not(".notification_state_pre-hidden");
            logger.debug("Detection of a notification witht the same id", extendedNotificationOptions.id, $notificationWithTheSameId, $notificationStack.children());
            if ($notificationWithTheSameId.length && ($notificationWithTheSameId.hasMod("notification", "state_shown") || $notificationWithTheSameId.hasMod("notification", "state_pre-shown"))) {
                if ($notificationWithTheSameId.offset().top < 0) {
                    removeNotification($notificationWithTheSameId);
                    addNewNotification(extendedNotificationOptions);
                } else {
                    updateNotification($notificationWithTheSameId, extendedNotificationOptions);
                }
            } else {
                addNewNotification(extendedNotificationOptions);
            }
        } else {
            addNewNotification(extendedNotificationOptions);
        }
        logger.debug("End showing a new notification", notificationOptions);
    };

    /**
     * Hide a notification
     */
    NotificationsModule.hide = function(notificationId) {
        var $notification = $notificationStack.find(_.str.sprintf(".notification[data-id='%s']", notificationId)).not(".notification_state_pre-hidden");
        if ($notification.length) {
            removeNotification($notification);
        }
    };

}, Logger);