Foxandxss/angular-toastr

View on GitHub
src/toastr.js

Summary

Maintainability
D
2 days
Test Coverage
(function() {
  'use strict';

  angular.module('toastr', [])
    .factory('toastr', toastr);

  toastr.$inject = ['$animate', '$injector', '$rootScope', '$sce', 'toastrConfig', '$q'];

  function toastr($animate, $injector, $rootScope, $sce, toastrConfig, $q) {
    var container;
    var index = 0;
    var toasts = [];

    var previousToastMessage = '';
    var openToasts = {};

    var containerDefer = $q.defer();

    var toast = {
      active: active,
      clear: clear,
      error: error,
      info: info,
      remove: remove,
      success: success,
      warning: warning,
      refreshTimer: refreshTimer
    };

    return toast;

    /* Public API */
    function active() {
      return toasts.length;
    }

    function clear(toast) {
      // Bit of a hack, I will remove this soon with a BC
      if (arguments.length === 1 && !toast) { return; }

      if (toast) {
        remove(toast.toastId);
      } else {
        for (var i = 0; i < toasts.length; i++) {
          remove(toasts[i].toastId);
        }
      }
    }

    function error(message, title, optionsOverride) {
      var type = _getOptions().iconClasses.error;
      return _buildNotification(type, message, title, optionsOverride);
    }

    function info(message, title, optionsOverride) {
      var type = _getOptions().iconClasses.info;
      return _buildNotification(type, message, title, optionsOverride);
    }

    function success(message, title, optionsOverride) {
      var type = _getOptions().iconClasses.success;
      return _buildNotification(type, message, title, optionsOverride);
    }

    function warning(message, title, optionsOverride) {
      var type = _getOptions().iconClasses.warning;
      return _buildNotification(type, message, title, optionsOverride);
    }

    function refreshTimer(toast, newTime) {
      if (toast && toast.isOpened && toasts.indexOf(toast) >= 0) {
          toast.scope.refreshTimer(newTime);
      }
    }

    function remove(toastId, wasClicked) {
      var toast = findToast(toastId);

      if (toast && ! toast.deleting) { // Avoid clicking when fading out
        toast.deleting = true;
        toast.isOpened = false;
        $animate.leave(toast.el).then(function() {
          if (toast.scope.options.onHidden) {
            toast.scope.options.onHidden(!!wasClicked, toast);
          }
          toast.scope.$destroy();
          var index = toasts.indexOf(toast);
          delete openToasts[toast.scope.message];
          toasts.splice(index, 1);
          var maxOpened = toastrConfig.maxOpened;
          if (maxOpened && toasts.length >= maxOpened) {
            toasts[maxOpened - 1].open.resolve();
          }
          if (lastToast()) {
            container.remove();
            container = null;
            containerDefer = $q.defer();
          }
        });
      }

      function findToast(toastId) {
        for (var i = 0; i < toasts.length; i++) {
          if (toasts[i].toastId === toastId) {
            return toasts[i];
          }
        }
      }

      function lastToast() {
        return !toasts.length;
      }
    }

    /* Internal functions */
    function _buildNotification(type, message, title, optionsOverride) {
      if (angular.isObject(title)) {
        optionsOverride = title;
        title = null;
      }

      return _notify({
        iconClass: type,
        message: message,
        optionsOverride: optionsOverride,
        title: title
      });
    }

    function _getOptions() {
      return angular.extend({}, toastrConfig);
    }

    function _createOrGetContainer(options) {
      if(container) { return containerDefer.promise; }

      container = angular.element('<div></div>');
      container.attr('id', options.containerId);
      container.addClass(options.positionClass);
      container.css({'pointer-events': 'auto'});

      var target = angular.element(document.querySelector(options.target));

      if ( ! target || ! target.length) {
        throw 'Target for toasts doesn\'t exist';
      }

      $animate.enter(container, target).then(function() {
        containerDefer.resolve();
      });

      return containerDefer.promise;
    }

    function _notify(map) {
      var options = _getOptions();

      if (shouldExit()) { return; }

      var newToast = createToast();

      toasts.push(newToast);

      if (ifMaxOpenedAndAutoDismiss()) {
        var oldToasts = toasts.slice(0, (toasts.length - options.maxOpened));
        for (var i = 0, len = oldToasts.length; i < len; i++) {
          remove(oldToasts[i].toastId);
        }
      }

      if (maxOpenedNotReached()) {
        newToast.open.resolve();
      }

      newToast.open.promise.then(function() {
        _createOrGetContainer(options).then(function() {
          newToast.isOpened = true;
          if (options.newestOnTop) {
            $animate.enter(newToast.el, container).then(function() {
              newToast.scope.init();
            });
          } else {
            var sibling = container[0].lastChild ? angular.element(container[0].lastChild) : null;
            $animate.enter(newToast.el, container, sibling).then(function() {
              newToast.scope.init();
            });
          }
        });
      });

      return newToast;

      function ifMaxOpenedAndAutoDismiss() {
        return options.autoDismiss && options.maxOpened && toasts.length > options.maxOpened;
      }

      function createScope(toast, map, options) {
        if (options.allowHtml) {
          toast.scope.allowHtml = true;
          toast.scope.title = $sce.trustAsHtml(map.title);
          toast.scope.message = $sce.trustAsHtml(map.message);
        } else {
          toast.scope.title = map.title;
          toast.scope.message = map.message;
        }

        toast.scope.toastType = toast.iconClass;
        toast.scope.toastId = toast.toastId;
        toast.scope.extraData = options.extraData;

        toast.scope.options = {
          extendedTimeOut: options.extendedTimeOut,
          messageClass: options.messageClass,
          onHidden: options.onHidden,
          onShown: generateEvent('onShown'),
          onTap: generateEvent('onTap'),
          progressBar: options.progressBar,
          tapToDismiss: options.tapToDismiss,
          timeOut: options.timeOut,
          titleClass: options.titleClass,
          toastClass: options.toastClass
        };

        if (options.closeButton) {
          toast.scope.options.closeHtml = options.closeHtml;
        }

        function generateEvent(event) {
          if (options[event]) {
            return function() {
              options[event](toast);
            };
          }
        }
      }

      function createToast() {
        var newToast = {
          toastId: index++,
          isOpened: false,
          scope: $rootScope.$new(),
          open: $q.defer()
        };
        newToast.iconClass = map.iconClass;
        if (map.optionsOverride) {
          angular.extend(options, cleanOptionsOverride(map.optionsOverride));
          newToast.iconClass = map.optionsOverride.iconClass || newToast.iconClass;
        }

        createScope(newToast, map, options);

        newToast.el = createToastEl(newToast.scope);

        return newToast;

        function cleanOptionsOverride(options) {
          var badOptions = ['containerId', 'iconClasses', 'maxOpened', 'newestOnTop',
                            'positionClass', 'preventDuplicates', 'preventOpenDuplicates', 'templates'];
          for (var i = 0, l = badOptions.length; i < l; i++) {
            delete options[badOptions[i]];
          }

          return options;
        }
      }

      function createToastEl(scope) {
        var angularDomEl = angular.element('<div toast></div>'),
          $compile = $injector.get('$compile');
        return $compile(angularDomEl)(scope);
      }

      function maxOpenedNotReached() {
        return options.maxOpened && toasts.length <= options.maxOpened || !options.maxOpened;
      }

      function shouldExit() {
        var isDuplicateOfLast = options.preventDuplicates && map.message === previousToastMessage;
        var isDuplicateOpen = options.preventOpenDuplicates && openToasts[map.message];

        if (isDuplicateOfLast || isDuplicateOpen) {
          return true;
        }

        previousToastMessage = map.message;
        openToasts[map.message] = true;

        return false;
      }
    }
  }
}());