Daniel@0: "use strict"; Daniel@0: /** Daniel@0: * Notification options: Daniel@0: * Daniel@0: * id Daniel@0: * null or string Daniel@0: * if the same as before, the old notification with the same id is replaced with the current one Daniel@0: * Daniel@0: * modifiers Daniel@0: * [string1, string2] Daniel@0: * bem modifiers (define look and feel) Daniel@0: * availabilities: Daniel@0: * no-padding Daniel@0: * type_warning Daniel@0: * type_error Daniel@0: * ttl_ever Daniel@0: * ttl_10 Daniel@0: * ttl_11 Daniel@0: * ttl_20 Daniel@0: * ... Daniel@0: * Daniel@0: * content Daniel@0: * text / jquery object Daniel@0: * what to show in the notification Daniel@0: * Daniel@0: * onClose Daniel@0: * function(options) Daniel@0: * what to do when the notification is manually closed Daniel@0: * Daniel@0: * onExpire Daniel@0: * function(options) Daniel@0: * what to do when the notification is removed automatically Daniel@0: * Daniel@0: * keepContentInMemoryAfterRemoval Daniel@0: * does not destroy the content on notification removal (it retains DOM nodes and event listeners, which can be reused later) Daniel@0: * Daniel@0: */ Daniel@0: App.module("NotificationsModule", function(NotificationsModule, App, Backbone, Marionette, $, _, Logger) { Daniel@0: // Prevent auto start Daniel@0: NotificationsModule.startWithParent = false; Daniel@0: Daniel@0: // Define options Daniel@0: var defaultModuleOptions = { Daniel@0: defaultTTL: 10, // time to live Daniel@0: minTTLOnUnfreeze: 6 // time to live after the notification is unfrozen (mouse went out) Daniel@0: }; Daniel@0: var moduleOptions; Daniel@0: Daniel@0: // Define private variables Daniel@0: var logger = null; Daniel@0: var $notifications = null; Daniel@0: var $notificationStack = null; Daniel@0: Daniel@0: // Define private methods Daniel@0: var addNewNotification = null; Daniel@0: var removeNotification = null; Daniel@0: var updateNotification = null; Daniel@0: Daniel@0: var notificationTimeoutHandler = null; Daniel@0: Daniel@0: /* Daniel@0: * Structure of "$notification" object Daniel@0: * .notification_body Daniel@0: * .notification_content Daniel@0: * .notification_closer Daniel@0: * data Daniel@0: * options Daniel@0: * ... Daniel@0: */ Daniel@0: Daniel@0: /** Daniel@0: * Initialization checker Daniel@0: */ Daniel@0: var assertModuleIsInitialized = function() { Daniel@0: if (!$notifications) { Daniel@0: throw "NotificationsModule has not been initialized"; Daniel@0: } Daniel@0: }; Daniel@0: Daniel@0: /** Daniel@0: * Module initializer Daniel@0: * Daniel@0: */ Daniel@0: NotificationsModule.addInitializer(function(options){ Daniel@0: moduleOptions = _.extend(defaultModuleOptions, options); Daniel@0: Daniel@0: logger = Logger.get("NotificationsModule"); Daniel@0: //logger.setLevel(Logger.DEBUG); Daniel@0: Daniel@0: logger.debug("Begin init"); Daniel@0: Daniel@0: $notifications = $(".app__notifications") Daniel@0: .addClass("notifications"); Daniel@0: Daniel@0: $notificationStack = $.bem.generateElement("notifications", "stack") Daniel@0: .appendTo($notifications); Daniel@0: Daniel@0: // Sets up the internals of the notification Daniel@0: var setupNotification = function($notification, notificationOptions) { Daniel@0: // Content Daniel@0: var $notificationContent = $notification.children().eq(0).children().eq(0); Daniel@0: if (!$notificationContent.hasClass("notification__content")) { Daniel@0: $notificationContent = $notificationContent.children().eq(0); Daniel@0: } Daniel@0: $notificationContent.children().detach(); Daniel@0: if (notificationOptions.content instanceof $) { Daniel@0: $notificationContent.append(notificationOptions.content); Daniel@0: } else { Daniel@0: $notificationContent.html(notificationOptions.content); Daniel@0: } Daniel@0: Daniel@0: $notification.attr("data-id", notificationOptions.id); Daniel@0: $notification.data("options", notificationOptions); Daniel@0: $notification.data("$content", $notificationContent); Daniel@0: Daniel@0: // Update css modifiers Daniel@0: var modifiersAsArray = null; Daniel@0: if (_.isString(notificationOptions.modifiers)) { Daniel@0: modifiersAsArray = notificationOptions.modifiers.split(" "); Daniel@0: } Daniel@0: if (_.isArray(notificationOptions.modifiers)){ Daniel@0: modifiersAsArray = notificationOptions.modifiers; Daniel@0: } Daniel@0: var state = $notification.getMod("notification", "state"); Daniel@0: $notification.attr("class", "notification notification_state_" + state); Daniel@0: if (_.isArray(modifiersAsArray) && modifiersAsArray.length) { Daniel@0: $notification.addClass("notification_" + modifiersAsArray.join(" notification_")); Daniel@0: } Daniel@0: Daniel@0: // Set ttl (time to live) if needed Daniel@0: if (!$notification.getMod("notification", "ttl")) { Daniel@0: $notification.setMod("notification", "ttl", moduleOptions.defaultTTL); Daniel@0: } Daniel@0: }; Daniel@0: Daniel@0: var freezeTTL = function () { Daniel@0: logger.debug("Freeze ttl", this); Daniel@0: $(this).addClass("notification_ttl-frozen"); Daniel@0: }; Daniel@0: var unfreezeTTL = function () { Daniel@0: logger.debug("Unfreeze ttl", this); Daniel@0: var $this = $(this); Daniel@0: var ttl = $this.getMod("notification", "ttl"); Daniel@0: if (ttl * 1 == ttl && ttl < moduleOptions.minTTLOnUnfreeze) { Daniel@0: $this.setMod("notification", "ttl", moduleOptions.minTTLOnUnfreeze); Daniel@0: } Daniel@0: $this.removeClass("notification_ttl-frozen"); Daniel@0: }; Daniel@0: var stopTTL = function () { Daniel@0: logger.debug("Stop ttl", this); Daniel@0: $(this).setMod("notification", "ttl", "ever"); Daniel@0: }; Daniel@0: Daniel@0: // A function that adds a new notification Daniel@0: addNewNotification = function(notificationOptions) { Daniel@0: logger.info("Adding new notificaton", notificationOptions); Daniel@0: Daniel@0: var $notificationContent = $.bem.generateElement("notification", "content"); Daniel@0: var $notificationCloser = $.bem.generateElement("notification", "closer"); Daniel@0: var $notificationBody = $.bem.generateElement("notification", "body") Daniel@0: .append($notificationContent, $notificationCloser); Daniel@0: Daniel@0: var $notification = $.bem.generateBlock("notification") Daniel@0: .append($notificationBody); Daniel@0: Daniel@0: var closeFunction = function() { Daniel@0: removeNotification($notification); Daniel@0: var onClose = notificationOptions.onClose; Daniel@0: if (_.isFunction(onClose)) { Daniel@0: try { Daniel@0: onClose($notification); Daniel@0: } catch (e) { Daniel@0: logger.error("Error when handling onClose", e, $notification, onClose); Daniel@0: } Daniel@0: } Daniel@0: }; Daniel@0: $notificationCloser.click(closeFunction); Daniel@0: $notification.mouseover(freezeTTL); Daniel@0: $notification.mouseout(unfreezeTTL); Daniel@0: $notification.click(stopTTL); Daniel@0: Daniel@0: setupNotification($notification, notificationOptions); Daniel@0: $notification.setMod("notification", "state", "pre-shown"); Daniel@0: $notification.appendTo($notificationStack); Daniel@0: Daniel@0: var notificationBodyHeight = $notificationBody.outerHeight(); Daniel@0: $notification.height(notificationBodyHeight); Daniel@0: $notification.hide(); Daniel@0: $notificationBody.hide(); Daniel@0: Daniel@0: // First show the container, and then slide the body Daniel@0: $notification.show("blind", { Daniel@0: direction: "up", Daniel@0: //easing: "easeInOutQuart" Daniel@0: Daniel@0: }, $notificationStack.children().length == 1 ? 1 : 100 + notificationBodyHeight / 10 * 30, function() { Daniel@0: $notificationBody.show("slide", { Daniel@0: direction: "right", Daniel@0: easing: "easeOutQuart" Daniel@0: }, 500, function() { Daniel@0: $notification.setMod("notification", "state", "shown"); Daniel@0: }); Daniel@0: }); Daniel@0: }; Daniel@0: Daniel@0: // A function that replaces the contents of the given notification Daniel@0: updateNotification = function($notification, notificationOptions) { Daniel@0: logger.info("Updating notification", $notification, "with new options", notificationOptions); Daniel@0: Daniel@0: if ($notification.length !== 1) { Daniel@0: logger.error(_.str.sprintf("Can’t replace a notification, this can be done only when $notification contains a single instance.", $notification)); Daniel@0: return; Daniel@0: } Daniel@0: Daniel@0: // Set up the notification again Daniel@0: setupNotification($notification, notificationOptions); Daniel@0: Daniel@0: // Pay user's attention to this notification Daniel@0: if (!$notification.is(":animated") && $notification.hasClass("notification_state_shown")) { Daniel@0: $notification.effect("shake", { Daniel@0: direction: "up", Daniel@0: distance: 5, Daniel@0: times: 2 Daniel@0: }); Daniel@0: } Daniel@0: }; Daniel@0: Daniel@0: // Removes the notification (hides it and removes) Daniel@0: removeNotification = function($notification) { Daniel@0: logger.debug("Begin remove notification", $notification); Daniel@0: if ($notification.hasMod("notification", "state_pre-hidden")) { Daniel@0: logger.debug("Notification", $notification, "is already being hidden by another handler"); Daniel@0: return; Daniel@0: }; Daniel@0: $notification.setMod("notification", "state", "pre-hidden"); Daniel@0: Daniel@0: // First fade the body and then blind the container Daniel@0: var $notificationBody = $notification.children().eq(0); Daniel@0: var notificationBodyHeight = $notificationBody.outerHeight(); Daniel@0: $notification.children().eq(0).fadeOut( Daniel@0: 200, Daniel@0: function() { Daniel@0: $notification.hide("blind", { Daniel@0: direction: "up", Daniel@0: }, 100 + notificationBodyHeight / 10 * 30, function() { Daniel@0: if ($notification.data("options").keepContentInMemoryAfterRemoval) { Daniel@0: $notification.data("$content").detach(); Daniel@0: } Daniel@0: $notification.remove(); Daniel@0: }); Daniel@0: }); Daniel@0: Daniel@0: }; Daniel@0: Daniel@0: // Every second decrease ttl (time to live) for each notification Daniel@0: notificationTimeoutHandler = setInterval(function() { Daniel@0: logger.debug("Notification lifetime interval triggered"); Daniel@0: $notificationStack Daniel@0: .children() Daniel@0: .filter(".notification_state_shown") Daniel@0: .not(".notification_ttl-frozen") // this class is set to notification when it is hovered over with a mouse Daniel@0: .each(function() { Daniel@0: var $thisNotification = $(this); Daniel@0: var ttl = $thisNotification.getMod("notification", "ttl"); Daniel@0: if (ttl * 1 != ttl) { Daniel@0: return; Daniel@0: } Daniel@0: ttl = ttl - 1; Daniel@0: if (ttl < 0) { Daniel@0: removeNotification($thisNotification); Daniel@0: var onExpire = $thisNotification.data("options").onExpire; Daniel@0: if (_.isFunction(onExpire)) { Daniel@0: try { Daniel@0: onExpire($thisNotification); Daniel@0: } catch (e) { Daniel@0: logger.error("Error when handling onExpire", e, $thisNotification, onExpire); Daniel@0: } Daniel@0: } Daniel@0: Daniel@0: } else { Daniel@0: $thisNotification.setMod("notification", "ttl", ttl); Daniel@0: } Daniel@0: }); Daniel@0: }, 1000); Daniel@0: Daniel@0: logger.debug("End init"); Daniel@0: }); Daniel@0: Daniel@0: /** Daniel@0: * Show a notification Daniel@0: */ Daniel@0: NotificationsModule.show = function(notificationOptions) { Daniel@0: assertModuleIsInitialized(); Daniel@0: logger.debug("Begin showing a new notification", notificationOptions); Daniel@0: Daniel@0: var extendedNotificationOptions = _.extend({ Daniel@0: id: null, Daniel@0: modifiers: null, Daniel@0: content: "", Daniel@0: onClose: null, Daniel@0: onExpire: null, Daniel@0: keepContentInMemoryAfterRemoval: false Daniel@0: }, notificationOptions); Daniel@0: Daniel@0: // Attempt to find and update notification with the same id (if it has been defined) Daniel@0: // Or to create a new notification Daniel@0: if (extendedNotificationOptions.id) { Daniel@0: var $notificationWithTheSameId = $notificationStack.find(_.str.sprintf(".notification[data-id='%s']", extendedNotificationOptions.id)).not(".notification_state_pre-hidden"); Daniel@0: logger.debug("Detection of a notification witht the same id", extendedNotificationOptions.id, $notificationWithTheSameId, $notificationStack.children()); Daniel@0: if ($notificationWithTheSameId.length && ($notificationWithTheSameId.hasMod("notification", "state_shown") || $notificationWithTheSameId.hasMod("notification", "state_pre-shown"))) { Daniel@0: if ($notificationWithTheSameId.offset().top < 0) { Daniel@0: removeNotification($notificationWithTheSameId); Daniel@0: addNewNotification(extendedNotificationOptions); Daniel@0: } else { Daniel@0: updateNotification($notificationWithTheSameId, extendedNotificationOptions); Daniel@0: } Daniel@0: } else { Daniel@0: addNewNotification(extendedNotificationOptions); Daniel@0: } Daniel@0: } else { Daniel@0: addNewNotification(extendedNotificationOptions); Daniel@0: } Daniel@0: logger.debug("End showing a new notification", notificationOptions); Daniel@0: }; Daniel@0: Daniel@0: /** Daniel@0: * Hide a notification Daniel@0: */ Daniel@0: NotificationsModule.hide = function(notificationId) { Daniel@0: var $notification = $notificationStack.find(_.str.sprintf(".notification[data-id='%s']", notificationId)).not(".notification_state_pre-hidden"); Daniel@0: if ($notification.length) { Daniel@0: removeNotification($notification); Daniel@0: } Daniel@0: }; Daniel@0: Daniel@0: }, Logger);