1/* global angular */ 2(function(window, document) { 3 'use strict'; 4 5 /* 6 * AngularJS Toaster 7 * Version: 2.0.0 8 * 9 * Copyright 2013-2016 Jiri Kavulak. 10 * All Rights Reserved. 11 * Use, reproduction, distribution, and modification of this code is subject to the terms and 12 * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php 13 * 14 * Author: Jiri Kavulak 15 * Related to project of John Papa, Hans Fjällemark and Nguyá» n Thiá»n Hùng (thienhung1989) 16 */ 17 18 angular.module('toaster', []).constant( 19 'toasterConfig', { 20 'limit': 0, // limits max number of toasts 21 'tap-to-dismiss': true, 22 'close-button': false, 23 'close-html': '<button class="toast-close-button" type="button">×</button>', 24 'newest-on-top': true, 25 'time-out': 5000, 26 'icon-classes': { 27 error: 'toast-error', 28 info: 'toast-info', 29 wait: 'toast-wait', 30 success: 'toast-success', 31 warning: 'toast-warning' 32 }, 33 'body-output-type': '', // Options: '', 'trustedHtml', 'template', 'templateWithData', 'directive' 34 'body-template': 'toasterBodyTmpl.html', 35 'icon-class': 'toast-info', 36 'position-class': 'toast-top-right', // Options (see CSS): 37 // 'toast-top-full-width', 'toast-bottom-full-width', 'toast-center', 38 // 'toast-top-left', 'toast-top-center', 'toast-top-right', 39 // 'toast-bottom-left', 'toast-bottom-center', 'toast-bottom-right', 40 'title-class': 'toast-title', 41 'message-class': 'toast-message', 42 'prevent-duplicates': false, 43 'mouseover-timer-stop': true // stop timeout on mouseover and restart timer on mouseout 44 } 45 ).service( 46 'toaster', [ 47 '$rootScope', 'toasterConfig', function($rootScope, toasterConfig) { 48 // http://stackoverflow.com/questions/26501688/a-typescript-guid-class 49 var Guid = (function() { 50 var Guid = {}; 51 Guid.newGuid = function() { 52 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 53 var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 54 return v.toString(16); 55 }); 56 }; 57 return Guid; 58 }()); 59 60 this.pop = function(type, title, body, timeout, bodyOutputType, clickHandler, toasterId, showCloseButton, toastId, onHideCallback) { 61 if (angular.isObject(type)) { 62 var params = type; // Enable named parameters as pop argument 63 this.toast = { 64 type: params.type, 65 title: params.title, 66 body: params.body, 67 timeout: params.timeout, 68 bodyOutputType: params.bodyOutputType, 69 clickHandler: params.clickHandler, 70 showCloseButton: params.showCloseButton, 71 closeHtml: params.closeHtml, 72 toastId: params.toastId, 73 onShowCallback: params.onShowCallback, 74 onHideCallback: params.onHideCallback, 75 directiveData: params.directiveData 76 }; 77 toasterId = params.toasterId; 78 } else { 79 this.toast = { 80 type: type, 81 title: title, 82 body: body, 83 timeout: timeout, 84 bodyOutputType: bodyOutputType, 85 clickHandler: clickHandler, 86 showCloseButton: showCloseButton, 87 toastId: toastId, 88 onHideCallback: onHideCallback 89 }; 90 } 91 92 if (!this.toast.toastId || !this.toast.toastId.length) { 93 this.toast.toastId = Guid.newGuid(); 94 } 95 96 $rootScope.$emit('toaster-newToast', toasterId, this.toast.toastId); 97 98 return { 99 toasterId: toasterId, 100 toastId: this.toast.toastId 101 }; 102 }; 103 104 this.clear = function(toasterId, toastId) { 105 if (angular.isObject(toasterId)) { 106 $rootScope.$emit('toaster-clearToasts', toasterId.toasterId, toasterId.toastId); 107 } else { 108 $rootScope.$emit('toaster-clearToasts', toasterId, toastId); 109 } 110 }; 111 112 // Create one method per icon class, to allow to call toaster.info() and similar 113 for (var type in toasterConfig['icon-classes']) { 114 this[type] = createTypeMethod(type); 115 } 116 117 function createTypeMethod(toasterType) { 118 return function(title, body, timeout, bodyOutputType, clickHandler, toasterId, showCloseButton, toastId, onHideCallback) { 119 if (angular.isString(title)) { 120 return this.pop( 121 toasterType, 122 title, 123 body, 124 timeout, 125 bodyOutputType, 126 clickHandler, 127 toasterId, 128 showCloseButton, 129 toastId, 130 onHideCallback); 131 } else { // 'title' is actually an object with options 132 return this.pop(angular.extend(title, { type: toasterType })); 133 } 134 }; 135 } 136 }] 137 ).factory( 138 'toasterEventRegistry', [ 139 '$rootScope', function($rootScope) { 140 var deregisterNewToast = null, deregisterClearToasts = null, newToastEventSubscribers = [], clearToastsEventSubscribers = [], toasterFactory; 141 142 toasterFactory = { 143 setup: function() { 144 if (!deregisterNewToast) { 145 deregisterNewToast = $rootScope.$on( 146 'toaster-newToast', function(event, toasterId, toastId) { 147 for (var i = 0, len = newToastEventSubscribers.length; i < len; i++) { 148 newToastEventSubscribers[i](event, toasterId, toastId); 149 } 150 }); 151 } 152 153 if (!deregisterClearToasts) { 154 deregisterClearToasts = $rootScope.$on( 155 'toaster-clearToasts', function(event, toasterId, toastId) { 156 for (var i = 0, len = clearToastsEventSubscribers.length; i < len; i++) { 157 clearToastsEventSubscribers[i](event, toasterId, toastId); 158 } 159 }); 160 } 161 }, 162 163 subscribeToNewToastEvent: function(onNewToast) { 164 newToastEventSubscribers.push(onNewToast); 165 }, 166 subscribeToClearToastsEvent: function(onClearToasts) { 167 clearToastsEventSubscribers.push(onClearToasts); 168 }, 169 unsubscribeToNewToastEvent: function(onNewToast) { 170 var index = newToastEventSubscribers.indexOf(onNewToast); 171 if (index >= 0) { 172 newToastEventSubscribers.splice(index, 1); 173 } 174 175 if (newToastEventSubscribers.length === 0) { 176 deregisterNewToast(); 177 deregisterNewToast = null; 178 } 179 }, 180 unsubscribeToClearToastsEvent: function(onClearToasts) { 181 var index = clearToastsEventSubscribers.indexOf(onClearToasts); 182 if (index >= 0) { 183 clearToastsEventSubscribers.splice(index, 1); 184 } 185 186 if (clearToastsEventSubscribers.length === 0) { 187 deregisterClearToasts(); 188 deregisterClearToasts = null; 189 } 190 } 191 }; 192 return { 193 setup: toasterFactory.setup, 194 subscribeToNewToastEvent: toasterFactory.subscribeToNewToastEvent, 195 subscribeToClearToastsEvent: toasterFactory.subscribeToClearToastsEvent, 196 unsubscribeToNewToastEvent: toasterFactory.unsubscribeToNewToastEvent, 197 unsubscribeToClearToastsEvent: toasterFactory.unsubscribeToClearToastsEvent 198 }; 199 }] 200 ) 201 .directive('directiveTemplate', ['$compile', '$injector', function($compile, $injector) { 202 return { 203 restrict: 'A', 204 scope: { 205 directiveName: '@directiveName', 206 directiveData: '@directiveData' 207 }, 208 replace: true, 209 link: function(scope, elm, attrs) { 210 scope.$watch('directiveName', function(directiveName) { 211 if (angular.isUndefined(directiveName) || directiveName.length <= 0) 212 throw new Error('A valid directive name must be provided via the toast body argument when using bodyOutputType: directive'); 213 214 var directive; 215 216 try { 217 directive = $injector.get(attrs.$normalize(directiveName) + 'Directive'); 218 } catch (e) { 219 throw new Error(directiveName + ' could not be found. ' + 220 'The name should appear as it exists in the markup, not camel
220Cased as it would appear in the directive declaration,' + 221 ' e.g. directive-name not directiveName.'); 222 } 223 224 225 var directiveDetails = directive[0]; 226 227 if (directiveDetails.scope !== true && directiveDetails.scope) { 228 throw new Error('Cannot use a directive with an isolated scope. ' + 229 'The scope must be either true or falsy (e.g. false/null/undefined). ' + 230 'Occurred for directive ' + directiveName + '.'); 231 } 232 233 if (directiveDetails.restrict.indexOf('A') < 0) { 234 throw new Error('Directives must be usable as attributes. ' + 235 'Add "A" to the restrict option (or remove the option entirely). Occurred for directive ' + 236 directiveName + '.'); 237 } 238 239 if (scope.directiveData) 240 scope.directiveData = angular.fromJson(scope.directiveData); 241 242 var template = $compile('<div ' + directiveName + '></div>')(scope); 243 244 elm.append(template); 245 }); 246 } 247 }; 248 }]) 249 .directive( 250 'toasterContainer', [ 251 '$parse', '$rootScope', '$interval', '$sce', 'toasterConfig', 'toaster', 'toasterEventRegistry', 252 function($parse, $rootScope, $interval, $sce, toasterConfig, toaster, toasterEventRegistry) { 253 return { 254 replace: true, 255 restrict: 'EA', 256 scope: true, // creates an internal scope for this directive (one per directive instance) 257 link: function(scope, elm, attrs) { 258 var mergedConfig; 259 260 // Merges configuration set in directive with default one 261 mergedConfig = angular.extend({}, toasterConfig, scope.$eval(attrs.toasterOptions)); 262 263 scope.config = { 264 toasterId: mergedConfig['toaster-id'], 265 position: mergedConfig['position-class'], 266 title: mergedConfig['title-class'], 267 message: mergedConfig['message-class'], 268 tap: mergedConfig['tap-to-dismiss'], 269 closeButton: mergedConfig['close-button'], 270 closeHtml: mergedConfig['close-html'], 271 animation: mergedConfig['animation-class'], 272 mouseoverTimer: mergedConfig['mouseover-timer-stop'] 273 }; 274 275 scope.$on( 276 "$destroy", function() { 277 toasterEventRegistry.unsubscribeToNewToastEvent(scope._onNewToast); 278 toasterEventRegistry.unsubscribeToClearToastsEvent(scope._onClearToasts); 279 } 280 ); 281 282 function setTimeout(toast, time) { 283 toast.timeoutPromise = $interval( 284 function() { 285 scope.removeToast(toast.toastId); 286 }, time, 1 287 ); 288 } 289 290 scope.configureTimer = function(toast) { 291 var timeout = angular.isNumber(toast.timeout) ? toast.timeout : mergedConfig['time-out']; 292 if (typeof timeout === "object") timeout = timeout[toast.type]; 293 if (timeout > 0) { 294 setTimeout(toast, timeout); 295 } 296 }; 297 298 function addToast(toast, toastId) { 299 toast.type = mergedConfig['icon-classes'][toast.type]; 300 if (!toast.type) { 301 toast.type = mergedConfig['icon-class']; 302 } 303 304 if (mergedConfig['prevent-duplicates'] === true && scope.toasters.length) { 305 if (scope.toasters[scope.toasters.length - 1].body === toast.body) { 306 return; 307 } else { 308 var i, len, dupFound = false;
309 for (i = 0, len = scope.toasters.length; i < len; i++) { 310 if (scope.toasters[i].toastId === toastId) { 311 dupFound = true; 312 break; 313 } 314 } 315 316 if (dupFound) return; 317 } 318 } 319 320 321 // set the showCloseButton property on the toast so that 322 // each template can bind directly to the property to show/hide 323 // the close button 324 var closeButton = mergedConfig['close-button']; 325 326 // if toast.showCloseButton is a boolean value, 327 // it was specifically overriden in the pop arguments 328 if (typeof toast.showCloseButton === "boolean") { 329 330 } else if (typeof closeButton === "boolean") { 331 toast.showCloseButton = closeButton; 332 } else if (typeof closeButton === "object") { 333 var closeButtonForType = closeButton[toast.type]; 334 335 if (typeof closeButtonForType !== "undefined" && closeButtonForType !== null) { 336 toast.showCloseButton = closeButtonForType; 337 } 338 } else { 339 // if an option was not set, default to false. 340 toast.showCloseButton = false; 341 } 342 343 if (toast.showCloseButton) { 344 toast.closeHtml = $sce.trustAsHtml(toast.closeHtml || scope.config.closeHtml); 345 } 346 347 // Set the toast.bodyOutputType to the default if it isn't set 348 toast.bodyOutputType = toast.bodyOutputType || mergedConfig['body-output-type']; 349 switch (toast.bodyOutputType) { 350 case 'trustedHtml': 351 toast.html = $sce.trustAsHtml(toast.body); 352 break; 353 case 'template': 354 toast.bodyTemplate = toast.body || mergedConfig['body-template']; 355 break; 356 case 'templateWithData': 357 var fcGet = $parse(toast.body || mergedConfig['body-template']); 358 var templateWithData = fcGet(scope); 359 toast.bodyTemplate = templateWithData.template; 360 toast.data = templateWithData.data; 361 break; 362 case 'directive': 363 toast.html = toast.body; 364 break; 365 } 366 367 scope.configureTimer(toast); 368 369 if (mergedConfig['newest-on-top'] === true) { 370 scope.toasters.unshift(toast); 371 if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) { 372 scope.toasters.pop(); 373 } 374 } else { 375 scope.toasters.push(toast); 376 if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) { 377 scope.toasters.shift(); 378 } 379 } 380 381 if (angular.isFunction(toast.onShowCallback)) { 382 toast.onShowCallback(); 383 } 384 } 385 386 scope.removeToast = function(toastId) { 387 var i, len; 388 for (i = 0, len = scope.toasters.length; i < len; i++) { 389 if (scope.toasters[i].toastId === toastId) { 390 removeToast(i); 391 break; 392 } 393 } 394 }; 395 396 function removeToast(toastIndex) { 397 var toast = scope.toasters[toastIndex]; 398 399 // toast is always defined since the index always has a match 400 if (toast.timeoutPromise) { 401 $interval.cancel(toast.timeoutPromise); 402 } 403 scope.toasters.splice(toastIndex, 1); 404 405 if (angular.isFunction(toast.onHideCallback)) { 406 toast.onHideCallback(); 407 } 408 } 409 410 function removeAllToasts(toastId) { 411 for (var i = scope.toasters.length - 1; i >= 0; i--) { 412 if (isUndefinedOrNull(toastId)) { 413 removeToast(i); 414 } else { 415 if (scope.toasters[i].toastId == toastId) { 416 removeToast(i); 417 } 418 } 419 } 420 } 421 422 scope.toasters = []; 423 424 function isUndefinedOrNull(val) { 425 return angular.isUndefined(val) || val === null; 426 } 427 428 scope._onNewToast = function(event, toasterId, toastId) { 429 // Compatibility: if toaster has no toasterId defined, and if call to display 430 // hasn't either, then the request is for us 431 432 if ((isUndefinedOrNull(scope.config.toasterId) && isUndefinedOrNull(toasterId)) || (!isUndefinedOrNull(scope.config.toasterId) && !isUndefinedOrNull(toasterId) && scope.config.toasterId == toasterId)) { 433 addToast(toaster.toast, toastId); 434 } 435 }; 436 scope._onClearToasts = function(event, toasterId, toastId) { 437 // Compatibility: if toaster has no toasterId defined, and if call to display 438 // hasn't either, then the request is for us 439 if (toasterId == '*' || (isUndefinedOrNull(scope.config.toasterId) && isUndefinedOrNull(toasterId)) || (!isUndefinedOrNull(scope.config.toasterId) && !isUndefinedOrNull(toasterId) && scope.config.toasterId == toasterId)) { 440 removeAllToasts(toastId); 441 } 442 }; 443 444 toasterEventRegistry.setup(); 445 446 toasterEventRegistry.subscribeToNewToastEvent(scope._onNewToast); 447 toasterEventRegistry.subscribeToClearToastsEvent(scope._onClearToasts); 448 }, 449 controller: [ 450 '$scope', '$element', '$attrs', function($scope, $element, $attrs) { 451 // Called on mouseover 452 $scope.stopTimer = function(toast) { 453 if ($scope.config.mouseoverTimer === true) { 454 if (toast.timeoutPromise) { 455 $interval.cancel(toast.timeoutPromise); 456 toast.timeoutPromise = null; 457 } 458 } 459 }; 460 461 // Called on mouseout 462 $scope.restartTimer = function(toast) { 463 if ($scope.config.mouseoverTimer === true) {
464 if (!toast.timeoutPromise) { 465 $scope.configureTimer(toast); 466 } 467 } else if (toast.timeoutPromise === null) { 468 $scope.removeToast(toast.toastId); 469 } 470 }; 471 472 $scope.click = function(toast, isCloseButton) { 473 if ($scope.config.tap === true || (toast.showCloseButton === true && isCloseButton === true)) { 474 var removeToast = true; 475 if (toast.clickHandler) { 476 if (angular.isFunction(toast.clickHandler)) { 477 removeToast = toast.clickHandler(toast, isCloseButton); 478 } else if (angular.isFunction($scope.$parent.$eval(toast.clickHandler))) { 479 removeToast = $scope.$parent.$eval(toast.clickHandler)(toast, isCloseButton); 480 } else { 481 console.log("TOAST-NOTE: Your click handler is not inside a parent scope of toaster-container."); 482 } 483 } 484 if (removeToast) { 485 $scope.removeToast(toast.toastId); 486 } 487 } 488 }; 489 }], 490 template: 491 '<div id="toast-container" ng-class="[config.position, config.animation]">' + 492 '<div ng-repeat="toaster in toasters" class="toast" ng-class="toaster.type" ng-click="click(toaster)" ng-mouseover="stopTimer(toaster)" ng-mouseout="restartTimer(toaster)">' + 493 '<div ng-if="toaster.showCloseButton" ng-click="click(toaster, true)" ng-bind-html="toaster.closeHtml"></div>' + 494 '<div ng-class="config.title">{{toaster.title}}</div>' + 495 '<div ng-class="config.message" ng-switch on="toaster.bodyOutputType">' + 496 '<div ng-switch-when="trustedHtml" ng-bind-html="toaster.html"></div>' + 497 '<div ng-switch-when="template"><div ng-include="toaster.bodyTemplate"></div></div>' + 498 '<div ng-switch-when="templateWithData"><div ng-include="toaster.bodyTemplate"></div></div>' + 499 '<div ng-switch-when="directive"><div directive-template directive-name="{{toaster.html}}" directive-data="{{toaster.directiveData}}"></div></div>' + 500 '<div ng-switch-default >{{toaster.body}}</div>' + 501 '</div>' + 502 '</div>' + 503 '</div>' 504 }; 505 }] 506 ); 507})(window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.