vendor: 4,994 bytes, lines 1-128
1/** 2 * @license AngularJS v1.2.6 3 * (c) 2010-2014 Google, Inc. http://angularjs.org 4 * License: MIT 5 */ 6(function(window, angular, undefined) {'use strict'; 7 8var $resourceMinErr = angular.$$minErr('$resource'); 9 10// Helper functions and regex to lookup a dotted path on an object 11// stopping at undefined/null. The path must be composed of ASCII 12// identifiers (just like $parse) 13var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/; 14 15function isValidDottedPath(path) { 16 return (path != null && path !== '' && path !== 'hasOwnProperty' && 17 MEMBER_NAME_REGEX.test('.' + path)); 18} 19 20function lookupDottedPath(obj, path) { 21 if (!isValidDottedPath(path)) { 22 throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); 23 } 24 var keys = path.split('.'); 25 for (var i = 0, ii = keys.length; i < ii && obj !== undefined; i++) { 26 var key = keys[i]; 27 obj = (obj !== null) ? obj[key] : undefined; 28 } 29 return obj; 30} 31 32/** 33 * Create a shallow copy of an object and clear other fields from the destination 34 */ 35function shallowClearAndCopy(src, dst) { 36 dst = dst || {}; 37 38 angular.forEach(dst, function(value, key){ 39 delete dst[key]; 40 }); 41 42 for (var key in src) { 43 if (src.hasOwnProperty(key) && key.charAt(0) !== '$' && key.charAt(1) !== '$') { 44 dst[key] = src[key]; 45 } 46 } 47 48 return dst; 49} 50 51/** 52 * @ngdoc overview 53 * @name ngResource 54 * @description 55 * 56 * # ngResource 57 * 58 * The `ngResource` module provides interaction support with RESTful services 59 * via the $resource service. 60 * 61 * {@installModule resource} 62 * 63 * <div doc-module-components="ngResource"></div> 64 * 65 * See {@link ngResource.$resource `$resource`} for usage. 66 */ 67 68/** 69 * @ngdoc object 70 * @name ngResource.$resource 71 * @requires $http 72 * 73 * @description 74 * A factory which creates a resource object that lets you interact with 75 * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. 76 * 77 * The returned resource object has action methods which provide high-level behaviors without 78 * the need to interact with the low level {@link ng.$http $http} service. 79 * 80 * Requires the {@link ngResource `ngResource`} module to be installed. 81 * 82 * @param {string} url A parametrized URL template with parameters prefixed by `:` as in 83 * `/user/:username`. If you are using a URL with a port number (e.g. 84 * `http://example.com:8080/api`), it will be respected. 85 * 86 * If you are using a url with a suffix, just add the suffix, like this: 87 * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` 88 * or even `$resource('http://example.com/resource/:resource_id.:format')` 89 * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be 90 * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you 91 * can escape it with `/\.`. 92 * 93 * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in 94 * `actions` methods. If any of the parameter value is a function, it will be executed every time 95 * when a param value needs to be obtained for a request (unless the param was overridden). 96 * 97 * Each key value in the parameter object is first bound to url template if present and then any 98 * excess keys are appended to the url seapph query after the `?`. 99 * 100 * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in 101 * URL `/path/greet?salutation=Hello`. 102 * 103 * If the parameter value is prefixed with `@` then the value of that parameter is extracted from 104 * the data object (useful for non-GET operations). 105 * 106 * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the 107 * default set of resource actions. The declaration should be created in the format of {@link 108 * ng.$http#usage_parameters $http.config}: 109 * 110 * {action1: {method:?, params:?, isArray:?, headers:?, ...}, 111 * action2: {method:?, params:?, isArray:?, headers:?, ...}, 112 * ...} 113 * 114 * Where: 115 * 116 * - **`action`** â {string} â The name of action. This name becomes the name of the method on 117 * your resource object. 118 * - **`method`** â {string} â HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, 119 * `DELETE`, and `JSONP`. 120 * - **`params`** â {Object=} â Optional set of pre-bound parameters for this action. If any of 121 * the parameter value is a function, it will be executed every time when a param value needs to 122 * be obtained for a request (unless the param was overridden). 123 * - **`url`** â {string} â action specific `url` override. The url templating is supported just 124 * like for the resource-level urls. 125 * - **`isArray`** â {boolean=} â If true then the returned object for this action is an array, 126 * see `returns` section. 127 * - **`transformRequest`** â 128 * `{function(data, headersGetter)|Array.<function(data, headersGetter)>
128}` â 129 * transform function or an array of such functions. The transform function takes the http 130 * request body and headers and returns its transformed (typically serialized) version. 131 * - **`transformResponse`** â 132 * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` â 133 * transform function or an array of such functions. The transform function takes the http 134 * response body and headers and returns its transformed (typically deserialized) version. 135 * - **`cache`** â `{boolean|Cache}` â If true, a default $http cache will be used to cache the 136 * GET request, otherwise if a cache instance built with 137 * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for 138 * caching. 139 * - **`timeout`** â `{number|Promise}` â timeout in milliseconds, or {@link ng.$q promise} that 140 * should abort the request when resolved. 141 * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the 142 * XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5 143 * requests with credentials} for more information. 144 * - **`responseType`** - `{string}` - see {@link 145 * https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}. 146 * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - 147 * `response` and `responseError`. Both `response` and `responseError` interceptors get called 148 * with `http response` object. See {@link ng.$http $http interceptors}. 149 * 150 * @returns {Object} A resource "class" object with methods for the default set of resource actions 151 * optionally extended with custom `actions`. The default set contains these actions: 152 * 153 * { 'get': {method:'GET'}, 154 * 'save': {method:'POST'}, 155 * 'query': {method:'GET', isArray:true}, 156 * 'remove': {method:'DELETE'}, 157 * 'delete': {method:'DELETE'} }; 158 * 159 * Calling these methods invoke an {@link ng.$http} with the specified http method, 160 * destination and parameters. When the data is returned from the server then the object is an 161 * instance of the resource class. The actions `save`, `remove` and `delete` are available on it 162 * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, 163 * read, update, delete) on server-side data like this: 164 * <pre> 165 var User = $resource('/user/:userId', {userId:'@id'}); 166 var user = User.get({userId:123}, function() { 167 user.abc = true; 168 user.$save(); 169 }); 170 </pre> 171 * 172 * It is important to realize that invoking a $resource object method immediately returns an 173 * empty reference (object or array depending on `isArray`). Once the data is returned from the 174 * server the existing reference is populated with the actual data. This is a useful trick since 175 * usually the resource is assigned to a model which is then rendered by the view. Having an empty 176 * object results in no rendering, once the data arrives from the server then the object is 177 * populated with the data and the view automatically re-renders itself showing the new data. This 178 * means that in most cases one never has to write a callback function for the action methods. 179 * 180 * The action methods on the class object or instance object can be invoked with the following 181 * parameters: 182 * 183 * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` 184 * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` 185 * - non-GET instance actions: `instance.$action([parameters], [success], [error])` 186 * 187 * Success callback is called with (value, responseHeaders) arguments. Error callback is called 188 * with (httpResponse) argument. 189 * 190 * Class actions return empty instance (with additional properties below). 191 * Instance actions return promise of the action. 192 * 193 * The Resource instances and collection have these additional properties: 194 * 195 * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this 196 * instance or collection. 197 * 198 * On success, the promise is resolved with the same resource instance or collection object, 199 * updated with data from server. This makes it easy to use in 200 * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view 201 * rendering until the resource(s) are loaded. 202 * 203 * On failure, the promise is resolved with the {@link ng.$http http response} object, without 204 * the `resource` property. 205 * 206 * - `$resolved`: `true` after first server interaction is completed (either with success or 207 * rejection), `false` before that. Knowing if the Resource has been resolved
207is useful in 208 * data-binding. 209 * 210 * @example 211 * 212 * # Credit card resource 213 * 214 * <pre> 215 // Define CreditCard class 216 var CreditCard = $resource('/user/:userId/card/:cardId', 217 {userId:123, cardId:'@id'}, { 218 charge: {method:'POST', params:{charge:true}} 219 }); 220 221 // We can retrieve a collection from the server 222 var cards = CreditCard.query(function() { 223 // GET: /user/123/card 224 // server returns: [ {id:456, number:'1234', name:'Smith'} ]; 225 226 var card = cards[0]; 227 // each item is an instance of CreditCard 228 expect(card instanceof CreditCard).toEqual(true); 229 card.name = "J. Smith"; 230 // non GET methods are mapped onto the instances 231 card.$save(); 232 // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} 233 // server returns: {id:456, number:'1234', name: 'J. Smith'}; 234 235 // our custom method is mapped as well. 236 card.$charge({amount:9.99}); 237 // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} 238 }); 239 240 // we can create an instance as well 241 var newCard = new CreditCard({number:'0123'}); 242 newCard.name = "Mike Smith"; 243 newCard.$save(); 244 // POST: /user/123/card {number:'0123', name:'Mike Smith'} 245 // server returns: {id:789, number:'0123', name: 'Mike Smith'}; 246 expect(newCard.id).toEqual(789); 247 * </pre> 248 * 249 * The object returned from this function execution is a resource "class" which has "static" method 250 * for each action in the definition. 251 * 252 * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and 253 * `headers`. 254 * When the data is returned from the server then the object is an instance of the resource type and 255 * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD 256 * operations (create, read, update, delete) on server-side data. 257 258 <pre> 259 var User = $resource('/user/:userId', {userId:'@id'}); 260 var user = User.get({userId:123}, function() { 261 user.abc = true; 262 user.$save(); 263 }); 264 </pre> 265 * 266 * It's worth noting that the success callback for `get`, `query` and other methods gets passed 267 * in the response that came from the server as well as $http header getter function, so one 268 * could rewrite the above example and get access to http headers as: 269 * 270 <pre> 271 var User = $resource('/user/:userId', {userId:'@id'}); 272 User.get({userId:123}, function(u, getResponseHeaders){ 273 u.abc = true; 274 u.$save(function(u, putResponseHeaders) { 275 //u => saved user object 276 //putResponseHeaders => $http header getter 277 }); 278 }); 279 </pre> 280 281 * # Creating a custom 'PUT' request 282 * In this example we create a custom method on our resource to make a PUT request 283 * <pre> 284 * var app = angular.module('app', ['ngResource', 'ngRoute']); 285 * 286 * // Some APIs expect a PUT request in the format URL/object/ID 287 * // Here we are creating an 'update' method 288 * app.factory('Notes', ['$resource', function($resource) { 289 * return $resource('/notes/:id', null, 290 * { 291 * 'update': { method:'PUT' } 292 * }); 293 * }]); 294 * 295 * // In our controller we get the ID from the URL using ngRoute and $routeParams 296 * // We pass in $routeParams and our Notes factory along with $scope 297 * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', 298 function($scope, $routeParams, Notes) { 299 * // First get a note object from the factory 300 * var note = Notes.get({ id:$routeParams.id }); 301 * $id = note.id; 302 * 303 * // Now call update passing in the ID first then the object you are updating 304 * Notes.update({ id:$id }, note); 305 * 306 * // This will PUT /notes/ID with the note object in the request payload 307 * }]); 308 * </pre> 309 */ 310angular.module('ngResource', ['ng']). 311 factory('$resource', ['$http', '$q', function($http, $q) { 312 313 var DEFAULT_ACTIONS = { 314 'get': {method:'GET'}, 315 'save': {method:'POST'}, 316 'query': {method:'GET', isArray:true}, 317 'remove': {method:'DELETE'}, 318 'delete': {method:'DELETE'} 319 }; 320 var noop = angular.noop,
321 forEach = angular.forEach, 322 extend = angular.extend, 323 copy = angular.copy, 324 isFunction = angular.isFunction; 325 326 /** 327 * We need our custom method because encodeURIComponent is too aggressive and doesn't follow 328 * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path 329 * segments: 330 * segment = *pchar 331 * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 332 * pct-encoded = "%" HEXDIG HEXDIG 333 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 334 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 335 * / "*" / "+" / "," / ";" / "=" 336 */ 337 function encodeUriSegment(val) { 338 return encodeUriQuery(val, true). 339 replace(/%26/gi, '&'). 340 replace(/%3D/gi, '='). 341 replace(/%2B/gi, '+'); 342 } 343 344 345 /** 346 * This method is intended for encoding *key* or *value* parts of query component. We need a 347 * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't 348 * have to be encoded per http://tools.ietf.org/html/rfc3986: 349 * query = *( pchar / "/" / "?" ) 350 * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 351 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 352 * pct-encoded = "%" HEXDIG HEXDIG 353 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 354 * / "*" / "+" / "," / ";" / "=" 355 */ 356 function encodeUriQuery(val, pctEncodeSpaces) { 357 return encodeURIComponent(val). 358 replace(/%40/gi, '@'). 359 replace(/%3A/gi, ':'). 360 replace(/%24/g, '$'). 361 replace(/%2C/gi, ','). 362 replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); 363 } 364 365 function Route(template, defaults) { 366 this.template = template; 367 this.defaults = defaults || {}; 368 this.urlParams = {}; 369 } 370 371 Route.prototype = { 372 setUrlParams: function(config, params, actionUrl) { 373 var self = this, 374 url = actionUrl || self.template, 375 val, 376 encodedVal; 377 378 var urlParams = self.urlParams = {}; 379 forEach(url.split(/\W/), function(param){ 380 if (param === 'hasOwnProperty') { 381 throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); 382 } 383 if (!(new RegExp("^\\d+$").test(param)) && param && 384 (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { 385 urlParams[param] = true; 386 } 387 }); 388 url = url.replace(/\\:/g, ':'); 389 390 params = params || {}; 391 forEach(self.urlParams, function(_, urlParam){ 392 val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; 393 if (angular.isDefined(val) && val !== null) { 394 encodedVal = encodeUriSegment(val); 395 url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), encodedVal + "$1"); 396 } else { 397 url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, 398 leadingSlashes, tail) { 399 if (tail.charAt(0) == '/') { 400 return tail; 401 } else { 402 return leadingSlashes + tail; 403 } 404 }); 405 } 406 }); 407 408 // strip trailing slashes and set the url 409 url = url.replace(/\/+$/, ''); 410 // then replace collapse `/.` if found in the last URL path segment before the query 411 // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` 412 url = url.replace(/\/\.(?=\w+($|\?))/, '.'); 413 // replace escaped `/\.` with `/.` 414 config.url = url.replace(/\/\\\./, '/.'); 415 416 417 // set params - delegate param encoding to $http 418 forEach(params, function(value, key){ 419 if (!self.urlParams[key]) { 420 config.params = config.params || {}; 421 config.params[key] = value; 422 } 423 }); 424 } 425 }; 426 427 428 function resourceFactory(url, paramDefaults, actions) { 429 var route = new Route(url); 430 431 actions = extend({}, DEFAULT_ACTIONS, actions); 432 433 function extractParams(data, actionParams){ 434 var ids = {}; 435 actionParams = extend({}, paramDefaults, actionParams);
436 forEach(actionParams, function(value, key){ 437 if (isFunction(value)) { value = value(); } 438 ids[key] = value && value.charAt && value.charAt(0) == '@' ? 439 lookupDottedPath(data, value.substr(1)) : value; 440 }); 441 return ids; 442 } 443 444 function defaultResponseInterceptor(response) { 445 return response.resource; 446 } 447 448 function Resource(value){ 449 shallowClearAndCopy(value || {}, this); 450 } 451 452 forEach(actions, function(action, name) { 453 var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); 454 455 Resource[name] = function(a1, a2, a3, a4) { 456 var params = {}, data, success, error; 457 458 /* jshint -W086 */ /* (purposefully fall through case statements) */ 459 switch(arguments.length) { 460 case 4: 461 error = a4; 462 success = a3; 463 //fallthrough 464 case 3: 465 case 2: 466 if (isFunction(a2)) { 467 if (isFunction(a1)) { 468 success = a1; 469 error = a2; 470 break; 471 } 472 473 success = a2; 474 error = a3; 475 //fallthrough 476 } else { 477 params = a1; 478 data = a2; 479 success = a3; 480 break; 481 } 482 case 1: 483 if (isFunction(a1)) success = a1; 484 else if (hasBody) data = a1; 485 else params = a1; 486 break; 487 case 0: break; 488 default: 489 throw $resourceMinErr('badargs', 490 "Expected up to 4 arguments [params, data, success, error], got {0} arguments", 491 arguments.length); 492 } 493 /* jshint +W086 */ /* (purposefully fall through case statements) */ 494 495 var isInstanceCall = this instanceof Resource; 496 var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); 497 var httpConfig = {}; 498 var responseInterceptor = action.interceptor && action.interceptor.response || 499 defaultResponseInterceptor; 500 var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || 501 undefined; 502 503 forEach(action, function(value, key) { 504 if (key != 'params' && key != 'isArray' && key != 'interceptor') { 505 httpConfig[key] = copy(value); 506 } 507 }); 508 509 if (hasBody) httpConfig.data = data; 510 route.setUrlParams(httpConfig, 511 extend({}, extractParams(data, action.params || {}), params), 512 action.url); 513 514 var promise = $http(httpConfig).then(function(response) { 515 var data = response.data, 516 promise = value.$promise; 517 518 if (data) { 519 // Need to convert action.isArray to boolean in case it is undefined 520 // jshint -W018 521 if (angular.isArray(data) !== (!!action.isArray)) { 522 throw $resourceMinErr('badcfg', 'Error in resource configuration. Expected ' + 523 'response to contain an {0} but got an {1}', 524 action.isArray?'array':'object', angular.isArray(data)?'array':'object'); 525 } 526 // jshint +W018 527 if (action.isArray) { 528 value.length = 0; 529 forEach(data, function(item) { 530 value.push(new Resource(item)); 531 }); 532 } else { 533 shallowClearAndCopy(data, value); 534 value.$promise = promise; 535 } 536 } 537 538 value.$resolved = true; 539 540 response.resource = value; 541 542 return response; 543 }, function(response) { 544 value.$resolved = true; 545 546 (error||noop)(response); 547 548 return $q.reject(response); 549 }); 550 551 promise = promise.then( 552 function(response) { 553 var value = responseInterceptor(response); 554 (success||noop)(value, response.headers); 555 return value; 556 }, 557 responseErrorInterceptor); 558 559 if (!isInstanceCall) { 560 // we are creating instance / collection 561 // - set the initial promise 562 // - return the instance / collection 563 value.$promise = promise; 564 value.$resolved = false;
565 566 return value; 567 } 568 569 // instance call 570 return promise; 571 }; 572 573 574 Resource.prototype['$' + name] = function(params, success, error) { 575 if (isFunction(params)) { 576 error = success; success = params; params = {}; 577 } 578 var result = Resource[name].call(this, params, this, success, error); 579 return result.$promise || result; 580 }; 581 }); 582 583 Resource.bind = function(additionalParamDefaults){ 584 return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); 585 }; 586 587 return Resource; 588 } 589 590 return resourceFactory; 591 }]); 592 593 594})(window, window.angular); 595 596/** 597 * @license AngularJS v1.2.6 598 * (c) 2010-2014 Google, Inc. http://angularjs.org 599 * License: MIT 600 */ 601(function(window, angular, undefined) {'use strict'; 602 603var $sanitizeMinErr = angular.$$minErr('$sanitize'); 604 605/** 606 * @ngdoc overview 607 * @name ngSanitize 608 * @description 609 * 610 * # ngSanitize 611 * 612 * The `ngSanitize` module provides functionality to sanitize HTML. 613 * 614 * {@installModule sanitize} 615 * 616 * <div doc-module-components="ngSanitize"></div> 617 * 618 * See {@link ngSanitize.$sanitize `$sanitize`} for usage. 619 */ 620 621/* 622 * HTML Parser By Misko Hevery ([email protected]) 623 * based on: HTML Parser By John Resig (ejohn.org) 624 * Original code by Erik Arvidsson, Mozilla Public License 625 * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 626 * 627 * // Use like so: 628 * htmlParser(htmlString, { 629 * start: function(tag, attrs, unary) {}, 630 * end: function(tag) {}, 631 * chars: function(text) {}, 632 * comment: function(text) {} 633 * }); 634 * 635 */ 636 637 638/** 639 * @ngdoc service 640 * @name ngSanitize.$sanitize 641 * @function 642 * 643 * @description 644 * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are 645 * then serialized back to properly escaped html string. This means that no unsafe input can make 646 * it into the returned string, however, since our parser is more strict than a typical browser 647 * parser, it's possible that some obscure input, which would be recognized as valid HTML by a 648 * browser, won't make it through the sanitizer. 649 * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and 650 * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. 651 * 652 * @param {string} html Html input. 653 * @returns {string} Sanitized html. 654 * 655 * @example 656 <doc:example module="ngSanitize"> 657 <doc:source> 658 <script> 659 function Ctrl($scope, $sce) { 660 $scope.snippet = 661 '<p style="color:blue">an html\n' + 662 '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' + 663 'snippet</p>'; 664 $scope.deliberatelyTrustDangerousSnippet = function() { 665 return $sce.trustAsHtml($scope.snippet); 666 }; 667 } 668 </script> 669 <div ng-controller="Ctrl"> 670 Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> 671 <table> 672 <tr> 673 <td>Directive</td> 674 <td>How</td> 675 <td>Source</td> 676 <td>Rendered</td> 677 </tr> 678 <tr id="bind-html-with-sanitize"> 679 <td>ng-bind-html</td> 680 <td>Automatically uses $sanitize</td> 681 <td><pre><div ng-bind-html="snippet"><br/></div></pre></td> 682 <td><div ng-bind-html="snippet"></div></td> 683 </tr> 684 <tr id="bind-html-with-trust"> 685 <td>ng-bind-html</td> 686 <td>Bypass $sanitize by explicitly trusting the dangerous value</td> 687 <td> 688 <pre><div ng-bind-html="deliberatelyTrustDangerousSnippet()"> 689</div></pre> 690 </td> 691 <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td> 692 </tr> 693 <tr id="bind-default"> 694 <td>ng-bind</td> 695 <td>Automatically escapes</td> 696 <td><pre><div ng-bind="snippet"><br/></div></pre></td> 697 <td><div ng-bind="snippet"></div></td> 698 </tr> 699 </table> 700 </div> 701 </doc:source> 702 <doc:scenario> 703 it('should sanitize the html snippet by default', function() {
704 expect(using('#bind-html-with-sanitize').element('div').html()). 705 toBe('<p>an html\n<em>click here</em>\nsnippet</p>'); 706 }); 707 708 it('should inline raw snippet if bound to a trusted value', function() { 709 expect(using('#bind-html-with-trust').element("div").html()). 710 toBe("<p style=\"color:blue\">an html\n" + 711 "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 712 "snippet</p>"); 713 }); 714 715 it('should escape snippet without any filter', function() { 716 expect(using('#bind-default').element('div').html()). 717 toBe("<p style=\"color:blue\">an html\n" + 718 "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 719 "snippet</p>"); 720 }); 721 722 it('should update', function() { 723 input('snippet').enter('new <b onclick="alert(1)">text</b>'); 724 expect(using('#bind-html-with-sanitize').element('div').html()).toBe('new <b>text</b>'); 725 expect(using('#bind-html-with-trust').element('div').html()).toBe( 726 'new <b onclick="alert(1)">text</b>'); 727 expect(using('#bind-default').element('div').html()).toBe( 728 "new <b onclick=\"alert(1)\">text</b>"); 729 }); 730 </doc:scenario> 731 </doc:example> 732 */ 733function $SanitizeProvider() { 734 this.$get = ['$$sanitizeUri', function($$sanitizeUri) { 735 return function(html) { 736 var buf = []; 737 htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { 738 return !/^unsafe/.test($$sanitizeUri(uri, isImage)); 739 })); 740 return buf.join(''); 741 }; 742 }]; 743} 744 745function sanitizeText(chars) { 746 var buf = []; 747 var writer = htmlSanitizeWriter(buf, angular.noop); 748 writer.chars(chars); 749 return buf.join(''); 750} 751 752 753// Regular Expressions for parsing tags and attributes 754var START_TAG_REGEXP = 755 /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, 756 END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, 757 ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, 758 BEGIN_TAG_REGEXP = /^</, 759 BEGING_END_TAGE_REGEXP = /^<\s*\//, 760 COMMENT_REGEXP = /<!--(.*?)-->/g, 761 DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i, 762 CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g, 763 // Match everything outside of normal chars and " (quote character) 764 NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; 765 766 767// Good source of info about elements and attributes 768// http://dev.w3.org/html5/spec/Overview.html#semantics 769// http://simon.html5.org/html-elements 770 771// Safe Void Elements - HTML5 772// http://dev.w3.org/html5/spec/Overview.html#void-elements 773var voidElements = makeMap("area,br,col,hr,img,wbr"); 774 775// Elements that you can, intentionally, leave open (and which close themselves) 776// http://dev.w3.org/html5/spec/Overview.html#optional-tags 777var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), 778 optionalEndTagInlineElements = makeMap("rp,rt"), 779 optionalEndTagElements = angular.extend({}, 780 optionalEndTagInlineElements, 781 optionalEndTagBlockElements); 782 783// Safe Block Elements - HTML5 784var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + 785 "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + 786 "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); 787 788// Inline Elements - HTML5 789var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + 790 "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + 791 "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); 792 793 794// Special Elements (can contain anything) 795var specialElements = makeMap("script,style"); 796 797var validElements = angular.extend({}, 798 voidElements, 799 blockElements, 800 inlineElements, 801 optionalEndTagElements); 802 803//Attributes that have href and hence need to be sanitized 804var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); 805var validAttrs = angular.extend({}, uriAttrs, makeMap( 806 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
vendor: 4,733 bytes, lines 807-972
807 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ 808 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ 809 'scope,scrolling,shape,span,start,summary,target,title,type,'+ 810 'valign,value,vspace,width')); 811 812function makeMap(str) { 813 var obj = {}, items = str.split(','), i; 814 for (i = 0; i < items.length; i++) obj[items[i]] = true; 815 return obj; 816} 817 818 819/** 820 * @example 821 * htmlParser(htmlString, { 822 * start: function(tag, attrs, unary) {}, 823 * end: function(tag) {}, 824 * chars: function(text) {}, 825 * comment: function(text) {} 826 * }); 827 * 828 * @param {string} html string 829 * @param {object} handler 830 */ 831function htmlParser( html, handler ) { 832 var index, chars, match, stack = [], last = html; 833 stack.last = function() { return stack[ stack.length - 1 ]; }; 834 835 while ( html ) { 836 chars = true; 837 838 // Make sure we're not in a script or style element 839 if ( !stack.last() || !specialElements[ stack.last() ] ) { 840 841 // Comment 842 if ( html.indexOf("<!--") === 0 ) { 843 // comments containing -- are not allowed unless they terminate the comment 844 index = html.indexOf("--", 4); 845 846 if ( index >= 0 && html.lastIndexOf("-->", index) === index) { 847 if (handler.comment) handler.comment( html.substring( 4, index ) ); 848 html = html.substring( index + 3 ); 849 chars = false; 850 } 851 // DOCTYPE 852 } else if ( DOCTYPE_REGEXP.test(html) ) { 853 match = html.match( DOCTYPE_REGEXP ); 854 855 if ( match ) { 856 html = html.replace( match[0] , ''); 857 chars = false; 858 } 859 // end tag 860 } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { 861 match = html.match( END_TAG_REGEXP ); 862 863 if ( match ) { 864 html = html.substring( match[0].length ); 865 match[0].replace( END_TAG_REGEXP, parseEndTag ); 866 chars = false; 867 } 868 869 // start tag 870 } else if ( BEGIN_TAG_REGEXP.test(html) ) { 871 match = html.match( START_TAG_REGEXP ); 872 873 if ( match ) { 874 html = html.substring( match[0].length ); 875 match[0].replace( START_TAG_REGEXP, parseStartTag ); 876 chars = false; 877 } 878 } 879 880 if ( chars ) { 881 index = html.indexOf("<"); 882 883 var text = index < 0 ? html : html.substring( 0, index ); 884 html = index < 0 ? "" : html.substring( index ); 885 886 if (handler.chars) handler.chars( decodeEntities(text) ); 887 } 888 889 } else { 890 html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), 891 function(all, text){ 892 text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); 893 894 if (handler.chars) handler.chars( decodeEntities(text) ); 895 896 return ""; 897 }); 898 899 parseEndTag( "", stack.last() ); 900 } 901 902 if ( html == last ) { 903 throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + 904 "of html: {0}", html); 905 } 906 last = html; 907 } 908 909 // Clean up any remaining tags 910 parseEndTag(); 911 912 function parseStartTag( tag, tagName, rest, unary ) { 913 tagName = angular.lowercase(tagName); 914 if ( blockElements[ tagName ] ) { 915 while ( stack.last() && inlineElements[ stack.last() ] ) { 916 parseEndTag( "", stack.last() ); 917 } 918 } 919 920 if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { 921 parseEndTag( "", tagName ); 922 } 923 924 unary = voidElements[ tagName ] || !!unary; 925 926 if ( !unary ) 927 stack.push( tagName ); 928 929 var attrs = {}; 930 931 rest.replace(ATTR_REGEXP, 932 function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { 933 var value = doubleQuotedValue 934 || singleQuotedValue 935 || unquotedValue 936 || ''; 937 938 attrs[name] = decodeEntities(value); 939 }); 940 if (handler.start) handler.start( tagName, attrs, unary ); 941 } 942 943 function parseEndTag( tag, tagName ) { 944 var pos = 0, i; 945 tagName = angular.lowercase(tagName); 946 if ( tagName ) 947 // Find the closest opened tag of the same type 948 for ( pos = stack.length - 1; pos >= 0; pos-- ) 949 if ( stack[ pos ] == tagName ) 950 break; 951 952 if ( pos >= 0 ) { 953 // Close all the open elements, up the stack 954 for ( i = stack.length - 1; i >= pos; i-- ) 955 if (handler.end) handler.end( stack[ i ] ); 956 957 // Remove the open elements from the stack 958 stack.length = pos; 959 } 960 } 961} 962 963var hiddenPre=document.createElement("pre"); 964var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; 965/** 966 * decodes all entities into regular string 967 * @param value 968 * @returns {string} A string with decoded entities. 969 */ 970function decodeEntities(value) { 971 if (!value) { return ''; } 972
973 // Note: IE8 does not preserve spaces at the start/end of innerHTML 974 // so we must capture them and reattach them afterward 975 var parts = spaceRe.exec(value); 976 var spaceBefore = parts[1]; 977 var spaceAfter = parts[3]; 978 var content = parts[2]; 979 if (content) { 980 hiddenPre.innerHTML=content.replace(/</g,"<"); 981 // innerText depends on styling as it doesn't display hidden elements. 982 // Therefore, it's better to use textContent not to cause unnecessary 983 // reflows. However, IE<9 don't support textContent so the innerText 984 // fallback is necessary. 985 content = 'textContent' in hiddenPre ? 986 hiddenPre.textContent : hiddenPre.innerText; 987 } 988 return spaceBefore + content + spaceAfter; 989} 990 991/** 992 * Escapes all potentially dangerous characters, so that the 993 * resulting string can be safely inserted into attribute or 994 * element text. 995 * @param value 996 * @returns escaped text 997 */ 998function encodeEntities(value) { 999 return value. 1000 replace(/&/g, '&'). 1001 replace(NON_ALPHANUMERIC_REGEXP, function(value){ 1002 return '&#' + value.charCodeAt(0) + ';'; 1003 }). 1004 replace(/</g, '<'). 1005 replace(/>/g, '>'); 1006} 1007 1008/** 1009 * create an HTML/XML writer which writes to buffer 1010 * @param {Array} buf use buf.jain('') to get out sanitized html string 1011 * @returns {object} in the form of { 1012 * start: function(tag, attrs, unary) {}, 1013 * end: function(tag) {}, 1014 * chars: function(text) {}, 1015 * comment: function(text) {} 1016 * } 1017 */ 1018function htmlSanitizeWriter(buf, uriValidator){ 1019 var ignore = false; 1020 var out = angular.bind(buf, buf.push); 1021 return { 1022 start: function(tag, attrs, unary){ 1023 tag = angular.lowercase(tag); 1024 if (!ignore && specialElements[tag]) { 1025 ignore = tag; 1026 }
vendor: 3,181 bytes, lines 1027-1133
1027 if (!ignore && validElements[tag] === true) { 1028 out('<'); 1029 out(tag); 1030 angular.forEach(attrs, function(value, key){ 1031 var lkey=angular.lowercase(key); 1032 var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); 1033 if (validAttrs[lkey] === true && 1034 (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { 1035 out(' '); 1036 out(key); 1037 out('="'); 1038 out(encodeEntities(value)); 1039 out('"'); 1040 } 1041 }); 1042 out(unary ? '/>' : '>'); 1043 } 1044 }, 1045 end: function(tag){ 1046 tag = angular.lowercase(tag); 1047 if (!ignore && validElements[tag] === true) { 1048 out('</'); 1049 out(tag); 1050 out('>'); 1051 } 1052 if (tag == ignore) { 1053 ignore = false; 1054 } 1055 }, 1056 chars: function(chars){ 1057 if (!ignore) { 1058 out(encodeEntities(chars)); 1059 } 1060 } 1061 }; 1062} 1063 1064 1065// define ngSanitize module and register $sanitize service 1066angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); 1067 1068/* global sanitizeText: false */ 1069 1070/** 1071 * @ngdoc filter 1072 * @name ngSanitize.filter:linky 1073 * @function 1074 * 1075 * @description 1076 * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and 1077 * plain email address links. 1078 * 1079 * Requires the {@link ngSanitize `ngSanitize`} module to be installed. 1080 * 1081 * @param {string} text Input text. 1082 * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. 1083 * @returns {string} Html-linkified text. 1084 * 1085 * @usage 1086 <span ng-bind-html="linky_expression | linky"></span> 1087 * 1088 * @example 1089 <doc:example module="ngSanitize"> 1090 <doc:source> 1091 <script> 1092 function Ctrl($scope) { 1093 $scope.snippet = 1094 'Pretty text with some links:\n'+ 1095 'http://angularjs.org/,\n'+ 1096 'mailto:[email protected],\n'+ 1097 '[email protected],\n'+ 1098 'and one more: ftp://127.0.0.1/.'; 1099 $scope.snippetWithTarget = 'http://angularjs.org/'; 1100 } 1101 </script> 1102 <div ng-controller="Ctrl"> 1103 Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> 1104 <table> 1105 <tr> 1106 <td>Filter</td> 1107 <td>Source</td> 1108 <td>Rendered</td> 1109 </tr> 1110 <tr id="linky-filter"> 1111 <td>linky filter</td> 1112 <td> 1113 <pre><div ng-bind-html="snippet | linky"><br></div></pre> 1114 </td> 1115 <td> 1116 <div ng-bind-html="snippet | linky"></div> 1117 </td> 1118 </tr> 1119 <tr id="linky-target"> 1120 <td>linky target</td> 1121 <td> 1122 <pre><div ng-bind-html="snippetWithTarget | linky:'_blank'"><br></div></pre> 1123 </td> 1124 <td> 1125 <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div> 1126 </td> 1127 </tr> 1128 <tr id="escaped-html"> 1129 <td>no filter</td> 1130 <td><pre><div ng-bind="snippet"><br></div></pre></td> 1131 <td><div ng-bind="snippet"></div></td> 1132 </tr> 1133 </table>
1134 </doc:source> 1135 <doc:scenario> 1136 it('should linkify the snippet with urls', function() { 1137 expect(using('#linky-filter').binding('snippet | linky')). 1138 toBe('Pretty text with some links: ' + 1139 '<a href="http://angularjs.org/">http://angularjs.org/</a>, ' + 1140 '<a href="mailto:[email protected]">[email protected]</a>, ' + 1141 '<a href="mailto:[email protected]">[email protected]</a>, ' + 1142 'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.'); 1143 }); 1144 1145 it ('should not linkify snippet without the linky filter', function() { 1146 expect(using('#escaped-html').binding('snippet')). 1147 toBe("Pretty text with some links:\n" + 1148 "http://angularjs.org/,\n" + 1149 "mailto:[email protected],\n" + 1150 "[email protected],\n" + 1151 "and one more: ftp://127.0.0.1/."); 1152 }); 1153 1154 it('should update', function() { 1155 input('snippet').enter('new http://link.'); 1156 expect(using('#linky-filter').binding('snippet | linky')). 1157 toBe('new <a href="http://link">http://link</a>.'); 1158 expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); 1159 }); 1160 1161 it('should work with the target property', function() { 1162 expect(using('#linky-target').binding("snippetWithTarget | linky:'_blank'")). 1163 toBe('<a target="_blank" href="http://angularjs.org/">http://angularjs.org/</a>'); 1164 }); 1165 </doc:scenario> 1166 </doc:example> 1167 */ 1168angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { 1169 var LINKY_URL_REGEXP = 1170 /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/, 1171 MAILTO_REGEXP = /^mailto:/; 1172 1173 return function(text, target) { 1174 if (!text) return text; 1175 var match; 1176 var raw = text; 1177 var html = []; 1178 var url; 1179 var i; 1180 while ((match = raw.match(LINKY_URL_REGEXP))) { 1181 // We can not end in these as they are sometimes found at the end of the sentence 1182 url = match[0]; 1183 // if we did not match ftp/http/mailto then assume mailto 1184 if (match[2] == match[3]) url = 'mailto:' + url; 1185 i = match.index;
1186 addText(raw.substr(0, i)); 1187 addLink(url, match[0].replace(MAILTO_REGEXP, '')); 1188 raw = raw.substring(i + match[0].length); 1189 } 1190 addText(raw); 1191 return $sanitize(html.join('')); 1192 1193 function addText(text) { 1194 if (!text) { 1195 return; 1196 } 1197 html.push(sanitizeText(text)); 1198 } 1199 1200 function addLink(url, text) { 1201 html.push('<a '); 1202 if (angular.isDefined(target)) { 1203 html.push('target="'); 1204 html.push(target); 1205 html.push('" '); 1206 } 1207 html.push('href="'); 1208 html.push(url); 1209 html.push('">'); 1210 addText(text); 1211 html.push('</a>'); 1212 } 1213 }; 1214}]); 1215 1216 1217})(window, window.angular); 1218 1219/** 1220 * @license AngularJS v1.2.6 1221 * (c) 2010-2014 Google, Inc. http://angularjs.org 1222 * License: MIT 1223 */ 1224(function(window, angular, undefined) {'use strict'; 1225 1226/** 1227 * @ngdoc overview 1228 * @name ngRoute 1229 * @description 1230 * 1231 * # ngRoute 1232 * 1233 * The `ngRoute` module provides routing and deeplinking services and directives for angular apps. 1234 * 1235 * ## Example 1236 * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. 1237 * 1238 * {@installModule route} 1239 * 1240 * <div doc-module-components="ngRoute"></div> 1241 */ 1242 /* global -ngRouteModule */ 1243var ngRouteModule = angular.module('ngRoute', ['ng']). 1244 provider('$route', $RouteProvider); 1245 1246/** 1247 * @ngdoc object 1248 * @name ngRoute.$routeProvider 1249 * @function 1250 * 1251 * @description 1252 *
1253 * Used for configuring routes. 1254 * 1255 * ## Example 1256 * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. 1257 * 1258 * ## Dependencies 1259 * Requires the {@link ngRoute `ngRoute`} module to be installed. 1260 */ 1261function $RouteProvider(){ 1262 function inherit(parent, extra) { 1263 return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra); 1264 } 1265 1266 var routes = {}; 1267 1268 /** 1269 * @ngdoc method 1270 * @name ngRoute.$routeProvider#when 1271 * @methodOf ngRoute.$routeProvider 1272 * 1273 * @param {string} path Route path (matched against `$location.path`). If `$location.path` 1274 * contains redundant trailing slash or is missing one, the route will still match and the 1275 * `$location.path` will be updated to add or drop the trailing slash to exactly match the 1276 * route definition. 1277 * 1278 * * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up 1279 * to the next slash are matched and stored in `$routeParams` under the given `name` 1280 * when the route matches. 1281 * * `path` can contain named groups starting with a colon and ending with a star: 1282 * e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name` 1283 * when the route matches. 1284 * * `path` can contain optional named groups with a question mark: e.g.`:name?`. 1285 * 1286 * For example, routes like `/color/:color/largecode/:largecode*\/edit` will match 1287 * `/color/brown/largecode/code/with/slashs/edit` and extract: 1288 * 1289 * * `color: brown` 1290 * * `largecode: code/with/slashs`. 1291 * 1292 * 1293 * @param {Object} route Mapping information to be assigned to `$route.current` on route 1294 * match. 1295 * 1296 * Object properties: 1297 * 1298 * - `controller` â `{(string|function()=}` â Controller fn that should be associated with 1299 * newly created scope or the name of a {@link angular.Module#controller registered 1300 * controller} if passed as a string. 1301 * - `controllerAs` â `{string=}` â A controller alias name. If present the controller will be 1302 * published to scope under the `controllerAs` name. 1303 * - `template` â `{string=|function()=}` â html template as a string or a function that 1304 * returns an html template as a string which should be used by {@link 1305 * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. 1306 * This property takes precedence over `templateUrl`. 1307 * 1308 * If `template` is a function, it will be called with the following parameters: 1309 * 1310 * - `{Array.<Object>}` - route parameters extracted from the current 1311 * `$location.path()` by applying the current route 1312 * 1313 * - `templateUrl` â `{string=|function()=}` â path or function that returns a path to an html 1314 * template that should be used by {@link ngRoute.directive:ngView ngView}. 1315 * 1316 * If `templateUrl` is a function, it will be called with the following parameters: 1317 * 1318 * - `{Array.<Object>}` - route parameters extracted from the current 1319 * `$location.path()` by applying the current route 1320 * 1321 * - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should 1322 * be injected into the controller. If any of these dependencies are promises, the router 1323 * will wait for them all to be resolved or one to be rejected before the controller is 1324 * instantiated. 1325 * If all the promises are resolved successfully, the values of the resolved promises are 1326 * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is 1327 * fired. If any of the promises are rejected the 1328 * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object 1329 * is: 1330 * 1331 * - `key` â `{string}`: a name of a dependency to be injected into the controller. 1332 * - `factory` - `{string|function}`: If `string` then it is an alias for a service. 1333 * Otherwise if function, then it is {@link api/AUTO.$injector#invoke injected} 1334 * and the return value is treated as the dependency. If the result is a promise, it is 1335 * resolved before its value is injected into the controller. Be aware that
1336 * `ngRoute.$routeParams` will still refer to the previous route within these resolve 1337 * functions. Use `$route.current.params` to access the new route parameters, instead. 1338 * 1339 * - `redirectTo` â {(string|function())=} â value to update 1340 * {@link ng.$location $location} path with and trigger route redirection. 1341 * 1342 * If `redirectTo` is a function, it will be called with the following parameters: 1343 * 1344 * - `{Object.<string>}` - route parameters extracted from the current 1345 * `$location.path()` by applying the current route templateUrl. 1346 * - `{string}` - current `$location.path()` 1347 * - `{Object}` - current `$location.search()` 1348 * 1349 * The custom `redirectTo` function is expected to return a string which will be used 1350 * to update `$location.path()` and `$location.search()`. 1351 * 1352 * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` 1353 * or `$location.hash()` changes. 1354 * 1355 * If the option is set to `false` and url in the browser changes, then 1356 * `$routeUpdate` event is broadcasted on the root scope. 1357 * 1358 * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive 1359 * 1360 * If the option is set to `true`, then the particular route can be matched without being 1361 * case sensitive 1362 * 1363 * @returns {Object} self 1364 * 1365 * @description 1366 * Adds a new route definition to the `$route` service. 1367 */ 1368 this.when = function(path, route) { 1369 routes[path] = angular.extend( 1370 {reloadOnSearch: true}, 1371 route, 1372 path && pathRegExp(path, route) 1373 ); 1374 1375 // create redirection for trailing slashes 1376 if (path) { 1377 var redirectPath = (path[path.length-1] == '/') 1378 ? path.substr(0, path.length-1) 1379 : path +'/'; 1380 1381 routes[redirectPath] = angular.extend( 1382 {redirectTo: path}, 1383 pathRegExp(redirectPath, route) 1384 ); 1385 } 1386 1387 return this; 1388 }; 1389 1390 /** 1391 * @param path {string} path 1392 * @param opts {Object} options 1393 * @return {?Object} 1394 * 1395 * @description 1396 * Normalizes the given path, returning a regular expression 1397 * and the original path. 1398 * 1399 * Inspired by pathRexp in visionmedia/express/lib/utils.js. 1400 */ 1401 function pathRegExp(path, opts) { 1402 var insensitive = opts.caseInsensitiveMatch, 1403 ret = { 1404 originalPath: path, 1405 regexp: path 1406 }, 1407 keys = ret.keys = []; 1408 1409 path = path 1410 .replace(/([().])/g, '\\$1') 1411 .replace(/(\/)?:(\w+)([\?|\*])?/g, function(_, slash, key, option){ 1412 var optional = option === '?' ? option : null; 1413 var star = option === '*' ? option : null; 1414 keys.push({ name: key, optional: !!optional }); 1415 slash = slash || ''; 1416 return '' 1417 + (optional ? '' : slash) 1418 + '(?:' 1419 + (optional ? slash : '') 1420 + (star && '(.+?)' || '([^/]+)') 1421 + (optional || '') 1422 + ')' 1423 + (optional || ''); 1424 }) 1425 .replace(/([\/$\*])/g, '\\$1'); 1426 1427 ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); 1428 return ret; 1429 } 1430 1431 /** 1432 * @ngdoc method 1433 * @name ngRoute.$routeProvider#otherwise 1434 * @methodOf ngRoute.$routeProvider 1435 * 1436 * @description 1437 * Sets route definition that will be used on route change when no other route definition 1438 * is matched. 1439 * 1440 * @param {Object} params Mapping information to be assigned to `$route.current`. 1441 * @returns {Object} self 1442 */ 1443 this.otherwise = function(params) { 1444 this.when(null, params); 1445 return this; 1446 }; 1447 1448 1449 this.$get = ['$rootScope', 1450 '$location', 1451 '$routeParams', 1452 '$q', 1453 '$injector', 1454 '$http', 1455 '$templateCache', 1456 '$sce', 1457 function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) { 1458 1459 /** 1460 * @ngdoc object 1461 * @name ngRoute.$route 1462 * @requires $location 1463 * @requires $routeParams 1464 * 1465 * @property {Object} current Reference to the current route definition. 1466 * The route definition contains: 1467 * 1468 * - `controller`: The controller constructor as define in route definition. 1469 * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for 1470 * controller instantiation. The `locals` contain 1471 * the resolved values of the `resolve` map. Additionally the `locals` also contain: 1472 * 1473 * - `$scope` - The current route scope. 1474 * - `$template` - The current route template HTML. 1475 * 1476 * @property {Array.<Object>} routes Array of all configured routes. 1477 *
1478 * @description 1479 * `$route` is used for deep-linking URLs to controllers and views (HTML partials). 1480 * It watches `$location.url()` and tries to map the path to an existing route definition. 1481 * 1482 * Requires the {@link ngRoute `ngRoute`} module to be installed. 1483 * 1484 * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API. 1485 * 1486 * The `$route` service is typically used in conjunction with the 1487 * {@link ngRoute.directive:ngView `ngView`} directive and the 1488 * {@link ngRoute.$routeParams `$routeParams`} service. 1489 * 1490 * @example 1491 This example shows how changing the URL hash causes the `$route` to match a route against the 1492 URL, and the `ngView` pulls in the partial. 1493 1494 Note that this example is using {@link ng.directive:script inlined templates} 1495 to get it working on jsfiddle as well. 1496 1497 <example module="ngViewExample" deps="angular-route.js"> 1498 <file name="index.html"> 1499 <div ng-controller="MainCntl"> 1500 Choose: 1501 <a href="Book/Moby">Moby</a> | 1502 <a href="Book/Moby/ch/1">Moby: Ch1</a> | 1503 <a href="Book/Gatsby">Gatsby</a> | 1504 <a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> | 1505 <a href="Book/Scarlet">Scarlet Letter</a><br/> 1506 1507 <div ng-view></div> 1508 <hr /> 1509 1510 <pre>$location.path() = {{$location.path()}}</pre> 1511 <pre>$route.current.templateUrl = {{$route.current.templateUrl}}</pre> 1512 <pre>$route.current.params = {{$route.current.params}}</pre> 1513 <pre>$route.current.scope.name = {{$route.current.scope.name}}</pre> 1514 <pre>$routeParams = {{$routeParams}}</pre> 1515 </div> 1516 </file> 1517 1518 <file name="book.html"> 1519 controller: {{name}}<br /> 1520 Book Id: {{params.bookId}}<br /> 1521 </file> 1522 1523 <file name="chapter.html"> 1524 controller: {{name}}<br /> 1525 Book Id: {{params.bookId}}<br /> 1526 Chapter Id: {{params.chapterId}} 1527 </file> 1528 1529 <file name="script.js"> 1530 angular.module('ngViewExample', ['ngRoute']) 1531 1532 .config(function($routeProvider, $locationProvider) { 1533 $routeProvider.when('/Book/:bookId', { 1534 templateUrl: 'book.html', 1535 controller: BookCntl, 1536 resolve: { 1537 // I will cause a 1 second delay 1538 delay: function($q, $timeout) { 1539 var delay = $q.defer(); 1540 $timeout(delay.resolve, 1000); 1541 return delay.promise; 1542 } 1543 } 1544 }); 1545 $routeProvider.when('/Book/:bookId/ch/:chapterId', { 1546 templateUrl: 'chapter.html', 1547 controller: ChapterCntl 1548 }); 1549 1550 // configure html5 to get links working on jsfiddle 1551 $locationProvider.html5Mode(true); 1552 }); 1553 1554 function MainCntl($scope, $route, $routeParams, $location) { 1555 $scope.$route = $route; 1556 $scope.$location = $location; 1557 $scope.$routeParams = $routeParams; 1558 } 1559 1560 function BookCntl($scope, $routeParams) { 1561 $scope.name = "BookCntl"; 1562 $scope.params = $routeParams; 1563 } 1564 1565 function ChapterCntl($scope, $routeParams) { 1566 $scope.name = "ChapterCntl"; 1567 $scope.params = $routeParams; 1568 } 1569 </file> 1570 1571 <file name="scenario.js"> 1572 it('should load and compile correct template', function() { 1573 element('a:contains("Moby: Ch1")').click(); 1574 var content = element('.doc-example-live [ng-view]').text(); 1575 expect(content).toMatch(/controller\: ChapterCntl/); 1576 expect(content).toMatch(/Book Id\: Moby/); 1577 expect(content).toMatch(/Chapter Id\: 1/); 1578 1579 element('a:contains("Scarlet")').click(); 1580 sleep(2); // promises are not part of scenario waiting 1581 content = element('.doc-example-live [ng-view]').text(); 1582 expect(content).toMatch(/controller\: BookCntl/); 1583 expect(content).toMatch(/Book Id\: Scarlet/); 1584 }); 1585 </file> 1586 </example> 1587 */ 1588 1589 /** 1590 * @ngdoc event 1591 * @name ngRoute.$route#$routeChangeStart 1592 * @eventOf ngRoute.$route 1593 * @eventType broadcast on root scope
1594 * @description 1595 * Broadcasted before a route change. At this point the route services starts 1596 * resolving all of the dependencies needed for the route change to occurs. 1597 * Typically this involves fetching the view template as well as any dependencies 1598 * defined in `resolve` route property. Once all of the dependencies are resolved 1599 * `$routeChangeSuccess` is fired. 1600 * 1601 * @param {Object} angularEvent Synthetic event object. 1602 * @param {Route} next Future route information. 1603 * @param {Route} current Current route information. 1604 */ 1605 1606 /** 1607 * @ngdoc event 1608 * @name ngRoute.$route#$routeChangeSuccess 1609 * @eventOf ngRoute.$route 1610 * @eventType broadcast on root scope 1611 * @description 1612 * Broadcasted after a route dependencies are resolved. 1613 * {@link ngRoute.directive:ngView ngView} listens for the directive 1614 * to instantiate the controller and render the view. 1615 * 1616 * @param {Object} angularEvent Synthetic event object. 1617 * @param {Route} current Current route information. 1618 * @param {Route|Undefined} previous Previous route information, or undefined if current is 1619 * first route entered. 1620 */ 1621 1622 /** 1623 * @ngdoc event 1624 * @name ngRoute.$route#$routeChangeError 1625 * @eventOf ngRoute.$route 1626 * @eventType broadcast on root scope 1627 * @description 1628 * Broadcasted if any of the resolve promises are rejected. 1629 * 1630 * @param {Object} angularEvent Synthetic event object 1631 * @param {Route} current Current route information. 1632 * @param {Route} previous Previous route information. 1633 * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. 1634 */ 1635 1636 /** 1637 * @ngdoc event 1638 * @name ngRoute.$route#$routeUpdate 1639 * @eventOf ngRoute.$route 1640 * @eventType broadcast on root scope 1641 * @description 1642 * 1643 * The `reloadOnSearch` property has been set to false, and we are reusing the same 1644 * instance of the Controller. 1645 */ 1646 1647 var forceReload = false, 1648 $route = { 1649 routes: routes, 1650 1651 /** 1652 * @ngdoc method 1653 * @name ngRoute.$route#reload 1654 * @methodOf ngRoute.$route 1655 * 1656 * @description 1657 * Causes `$route` service to reload the current route even if 1658 * {@link ng.$location $location} hasn't changed. 1659 * 1660 * As a result of that, {@link ngRoute.directive:ngView ngView} 1661 * creates new scope, reinstantiates the controller. 1662 */ 1663 reload: function() { 1664 forceReload = true; 1665 $rootScope.$evalAsync(updateRoute); 1666 } 1667 }; 1668 1669 $rootScope.$on('$locationChangeSuccess', updateRoute); 1670 1671 return $route; 1672 1673 ///////////////////////////////////////////////////// 1674 1675 /** 1676 * @param on {string} current url 1677 * @param route {Object} route regexp to match the url against 1678 * @return {?Object} 1679 * 1680 * @description 1681 * Check if the route matches the current url. 1682 * 1683 * Inspired by match in 1684 * visionmedia/express/lib/router/router.js. 1685 */ 1686 function switchRouteMatcher(on, route) { 1687 var keys = route.keys, 1688 params = {}; 1689 1690 if (!route.regexp) return null; 1691 1692 var m = route.regexp.exec(on); 1693 if (!m) return null; 1694 1695 for (var i = 1, len = m.length; i < len; ++i) { 1696 var key = keys[i - 1]; 1697 1698 var val = 'string' == typeof m[i] 1699 ? decodeURIComponent(m[i]) 1700 : m[i]; 1701 1702 if (key && val) { 1703 params[key.name] = val; 1704 } 1705 } 1706 return params; 1707 } 1708 1709 function updateRoute() { 1710 var next = parseRoute(), 1711 last = $route.current; 1712 1713 if (next && last && next.$$route === last.$$route 1714 && angular.equals(next.pathParams, last.pathParams) 1715 && !next.reloadOnSearch && !forceReload) { 1716 last.params = next.params; 1717 angular.copy(last.params, $routeParams); 1718 $rootScope.$broadcast('$routeUpdate', last); 1719 } else if (next || last) { 1720 forceReload = false;
1721 $rootScope.$broadcast('$routeChangeStart', next, last); 1722 $route.current = next; 1723 if (next) { 1724 if (next.redirectTo) { 1725 if (angular.isString(next.redirectTo)) { 1726 $location.path(interpolate(next.redirectTo, next.params)).search(next.params) 1727 .replace(); 1728 } else { 1729 $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search())) 1730 .replace(); 1731 } 1732 } 1733 } 1734 1735 $q.when(next). 1736 then(function() { 1737 if (next) { 1738 var locals = angular.extend({}, next.resolve), 1739 template, templateUrl; 1740 1741 angular.forEach(locals, function(value, key) { 1742 locals[key] = angular.isString(value) ? 1743 $injector.get(value) : $injector.invoke(value); 1744 }); 1745 1746 if (angular.isDefined(template = next.template)) { 1747 if (angular.isFunction(template)) { 1748 template = template(next.params); 1749 } 1750 } else if (angular.isDefined(templateUrl = next.templateUrl)) { 1751 if (angular.isFunction(templateUrl)) { 1752 templateUrl = templateUrl(next.params); 1753 } 1754 templateUrl = $sce.getTrustedResourceUrl(templateUrl); 1755 if (angular.isDefined(templateUrl)) { 1756 next.loadedTemplateUrl = templateUrl; 1757 template = $http.get(templateUrl, {cache: $templateCache}). 1758 then(function(response) { return response.data; }); 1759 } 1760 } 1761 if (angular.isDefined(template)) { 1762 locals['$template'] = template; 1763 } 1764 return $q.all(locals); 1765 } 1766 }). 1767 // after route change 1768 then(function(locals) { 1769 if (next == $route.current) { 1770 if (next) { 1771 next.locals = locals; 1772 angular.copy(next.params, $routeParams); 1773 } 1774 $rootScope.$broadcast('$routeChangeSuccess', next, last); 1775 } 1776 }, function(error) { 1777 if (next == $route.current) { 1778 $rootScope.$broadcast('$routeChangeError', next, last, error); 1779 } 1780 }); 1781 } 1782 } 1783 1784 1785 /** 1786 * @returns the current active route, by matching it against the URL 1787 */ 1788 function parseRoute() { 1789 // Match a route 1790 var params, match; 1791 angular.forEach(routes, function(route, path) { 1792 if (!match && (params = switchRouteMatcher($location.path(), route))) { 1793 match = inherit(route, { 1794 params: angular.extend({}, $location.search(), params), 1795 pathParams: params}); 1796 match.$$route = route; 1797 } 1798 }); 1799 // No route matched; fallback to "otherwise" route 1800 return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); 1801 } 1802 1803 /** 1804 * @returns interpolation of the redirect path with the parameters 1805 */ 1806 function interpolate(string, params) { 1807 var result = []; 1808 angular.forEach((string||'').split(':'), function(segment, i) { 1809 if (i === 0) { 1810 result.push(segment); 1811 } else { 1812 var segmentMatch = segment.match(/(\w+)(.*)/); 1813 var key = segmentMatch[1]; 1814 result.push(params[key]); 1815 result.push(segmentMatch[2] || ''); 1816 delete params[key]; 1817 } 1818 }); 1819 return result.join(''); 1820 } 1821 }]; 1822} 1823 1824ngRouteModule.provider('$routeParams', $RouteParamsProvider); 1825 1826 1827/** 1828 * @ngdoc object 1829 * @name ngRoute.$routeParams 1830 * @requires $route 1831 * 1832 * @description 1833 * The `$routeParams` service allows you to retrieve the current set of route parameters. 1834 * 1835 * Requires the {@link ngRoute `ngRoute`} module to be installed. 1836 * 1837 * The route parameters are a combination of {@link ng.$location `$location`}'s 1838 * {@link ng.$location#methods_search `search()`} and {@link ng.$location#methods_path `path()`}. 1839 * The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched. 1840 * 1841 * In case of parameter name collision, `path` params take precedence over `search` params. 1842 *
1843 * The service guarantees that the identity of the `$routeParams` object will remain unchanged 1844 * (but its properties will likely change) even when a route change occurs. 1845 * 1846 * Note that the `$routeParams` are only updated *after* a route change completes successfully. 1847 * This means that you cannot rely on `$routeParams` being correct in route resolve functions. 1848 * Instead you can use `$route.current.params` to access the new route's parameters. 1849 * 1850 * @example 1851 * <pre> 1852 * // Given: 1853 * // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby 1854 * // Route: /Chapter/:chapterId/Section/:sectionId 1855 * // 1856 * // Then 1857 * $routeParams ==> {chapterId:1, sectionId:2, search:'moby'} 1858 * </pre> 1859 */ 1860function $RouteParamsProvider() { 1861 this.$get = function() { return {}; }; 1862} 1863 1864ngRouteModule.directive('ngView', ngViewFactory); 1865ngRouteModule.directive('ngView', ngViewFillContentFactory); 1866 1867 1868/** 1869 * @ngdoc directive 1870 * @name ngRoute.directive:ngView 1871 * @restrict ECA 1872 * 1873 * @description 1874 * # Overview 1875 * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by 1876 * including the rendered template of the current route into the main layout (`index.html`) file. 1877 * Every time the current route changes, the included view changes with it according to the 1878 * configuration of the `$route` service. 1879 * 1880 * Requires the {@link ngRoute `ngRoute`} module to be installed. 1881 * 1882 * @animations 1883 * enter - animation is used to bring new content into the browser. 1884 * leave - animation is used to animate existing content away. 1885 * 1886 * The enter and leave animation occur concurrently. 1887 * 1888 * @scope 1889 * @priority 400 1890 * @example 1891 <example module="ngViewExample" deps="angular-route.js" animations="true"> 1892 <file name="index.html"> 1893 <div ng-controller="MainCntl as main"> 1894 Choose: 1895 <a href="Book/Moby">Moby</a> | 1896 <a href="Book/Moby/ch/1">Moby: Ch1</a> | 1897 <a href="Book/Gatsby">Gatsby</a> | 1898 <a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> | 1899 <a href="Book/Scarlet">Scarlet Letter</a><br/> 1900 1901 <div class="view-animate-container"> 1902 <div ng-view class="view-animate"></div> 1903 </div> 1904 <hr /> 1905 1906 <pre>$location.path() = {{main.$location.path()}}</pre> 1907 <pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre> 1908 <pre>$route.current.params = {{main.$route.current.params}}</pre> 1909 <pre>$route.current.scope.name = {{main.$route.current.scope.name}}</pre> 1910 <pre>$routeParams = {{main.$routeParams}}</pre> 1911 </div> 1912 </file> 1913 1914 <file name="book.html"> 1915 <div> 1916 controller: {{book.name}}<br /> 1917 Book Id: {{book.params.bookId}}<br /> 1918 </div> 1919 </file> 1920 1921 <file name="chapter.html"> 1922 <div> 1923 controller: {{chapter.name}}<br /> 1924 Book Id: {{chapter.params.bookId}}<br /> 1925 Chapter Id: {{chapter.params.chapterId}} 1926 </div> 1927 </file> 1928 1929 <file name="animations.css"> 1930 .view-animate-container { 1931 position:relative; 1932 height:100px!important; 1933 position:relative; 1934 background:white; 1935 border:1px solid black; 1936 height:40px; 1937 overflow:hidden; 1938 } 1939 1940 .view-animate { 1941 padding:10px; 1942 } 1943 1944 .view-animate.ng-enter, .view-animate.ng-leave { 1945 -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; 1946 transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; 1947 1948 display:block; 1949 width:100%; 1950 border-left:1px solid black; 1951 1952 position:absolute; 1953 top:0; 1954 left:0; 1955 right:0; 1956 bottom:0; 1957 padding:10px; 1958 } 1959 1960 .view-animate.ng-enter { 1961 left:100%; 1962 } 1963 .view-animate.ng-enter.ng-enter-active { 1964 left:0; 1965 } 1966 .view-animate.ng-leave.ng-leave-active { 1967 left:-100%; 1968 } 1969 </file> 1970 1971 <file name="script.js"> 1972 angular.module('ngViewExample', ['ngRoute', 'ngAnimate'], 1973 function($routeProvider, $locationProvider) { 1974 $routeProvider.when('/Book/:bookId', { 1975 templateUrl: 'book.html', 1976 controller: BookCntl, 1977 controllerAs: 'book' 1978 }); 1979 $routeProvider.when('/Book/:bookId/ch/:chapterId', { 1980 templateUrl: 'chapter.html', 1981 controller: ChapterCntl, 1982 controllerAs: 'chapter' 1983 }); 1984 1985 // configure html5 to get links working on jsfiddle 1986 $locationProvider.html5Mode(true); 1987 }); 1988
1989 function MainCntl($route, $routeParams, $location) { 1990 this.$route = $route; 1991 this.$location = $location; 1992 this.$routeParams = $routeParams; 1993 } 1994 1995 function BookCntl($routeParams) { 1996 this.name = "BookCntl"; 1997 this.params = $routeParams; 1998 } 1999 2000 function ChapterCntl($routeParams) { 2001 this.name = "ChapterCntl"; 2002 this.params = $routeParams; 2003 } 2004 </file> 2005 2006 <file name="scenario.js"> 2007 it('should load and compile correct template', function() { 2008 element('a:contains("Moby: Ch1")').click(); 2009 var content = element('.doc-example-live [ng-view]').text(); 2010 expect(content).toMatch(/controller\: ChapterCntl/); 2011 expect(content).toMatch(/Book Id\: Moby/); 2012 expect(content).toMatch(/Chapter Id\: 1/); 2013 2014 element('a:contains("Scarlet")').click(); 2015 content = element('.doc-example-live [ng-view]').text(); 2016 expect(content).toMatch(/controller\: BookCntl/); 2017 expect(content).toMatch(/Book Id\: Scarlet/); 2018 }); 2019 </file> 2020 </example> 2021 */ 2022 2023 2024/** 2025 * @ngdoc event 2026 * @name ngRoute.directive:ngView#$viewContentLoaded 2027 * @eventOf ngRoute.directive:ngView 2028 * @eventType emit on the current ngView scope 2029 * @description 2030 * Emitted every time the ngView content is reloaded. 2031 */ 2032ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; 2033function ngViewFactory( $route, $anchorScroll, $animate) { 2034 return { 2035 restrict: 'ECA', 2036 terminal: true, 2037 priority: 400, 2038 transclude: 'element', 2039 link: function(scope, $element, attr, ctrl, $transclude) { 2040 var currentScope, 2041 currentElement, 2042 autoScrollExp = attr.autoscroll, 2043 onloadExp = attr.onload || ''; 2044 2045 scope.$on('$routeChangeSuccess', update); 2046 update(); 2047 2048 function cleanupLastView() { 2049 if (currentScope) { 2050 currentScope.$destroy(); 2051 currentScope = null; 2052 } 2053 if(currentElement) { 2054 $animate.leave(currentElement); 2055 currentElement = null; 2056 } 2057 } 2058 2059 function update() { 2060 var locals = $route.current && $route.current.locals, 2061 template = locals && locals.$template; 2062 2063 if (template) { 2064 var newScope = scope.$new(); 2065 var current = $route.current; 2066 2067 // Note: This will also link all children of ng-view that were contained in the original 2068 // html. If that content contains controllers, ... they could pollute/change the scope. 2069 // However, using ng-view on an element with additional content does not make sense... 2070 // Note: We can't remove them in the cloneAttchFn of $transclude as that 2071 // function is called before linking the content, which would apply child 2072 // directives to non existing elements. 2073 var clone = $transclude(newScope, function(clone) { 2074 $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () { 2075 if (angular.isDefined(autoScrollExp) 2076 && (!autoScrollExp || scope.$eval(autoScrollExp))) { 2077 $anchorScroll(); 2078 } 2079 }); 2080 cleanupLastView(); 2081 }); 2082 2083 currentElement = clone; 2084 currentScope = current.scope = newScope; 2085 currentScope.$emit('$viewContentLoaded'); 2086 currentScope.$eval(onloadExp); 2087 } else { 2088 cleanupLastView(); 2089 } 2090 } 2091 } 2092 }; 2093} 2094 2095// This directive is called during the $transclude call of the first `ngView` directive. 2096// It will replace and compile the content of the element with the loaded template. 2097// We need this directive so that the element content is already filled when 2098// the link function of another directive on the same element as ngView 2099// is called. 2100ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route']; 2101function ngViewFillContentFactory($compile, $controller, $route) { 2102 return { 2103 restrict: 'ECA', 2104 priority: -400, 2105 link: function(scope, $element) { 2106 var current = $route.current, 2107 locals = current.locals; 2108 2109 $element.html(locals.$template); 2110 2111 var link = $compile($element.contents()); 2112 2113 if (current.controller) { 2114 locals.$scope = scope; 2115 var controller = $controller(current.controller, locals); 2116 if (current.controllerAs) { 2117 scope[current.controllerAs] = controller; 2118 } 2119 $element.data('$ngControllerController', controller); 2120 $element.children().data('$ngControllerController', controller); 2121 } 2122 2123 link(scope); 2124 } 2125 }; 2126} 2127 2128 2129})(window, window.angular); 2130 2131angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); 2132angular.module("ui.bootstrap.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/popup.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset-titles.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]); 2133angular.module('ui.bootstrap.transition', []) 2134 2135/** 2136 * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. 2137 * @param {DOMElement} element The DOMElement that will be animated. 2138 * @param {string|object|function} trigger The thing that will cause the transition to start: 2139 * - As a string, it represents the css class to be added to the element. 2140 * - As an object, it represents a hash of style attributes to be applied to the element. 2141 * - As a function, it represents a function to be called that will cause the transition to occur. 2142 * @return {Promise} A promise that is resolved when the transition finishes. 2143 */ 2144.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { 2145 2146 var $transition = function(element, trigger, options) { 2147 options = options || {}; 2148 var deferred = $q.defer(); 2149 var endEventName = $transition[options.animation ? "animationEndEventName" : "transitionEndEventName"]; 2150 2151 var transitionEndHandler = function(event) { 2152 $rootScope.$apply(function() { 2153 element.unbind(endEventName, transitionEndHandler); 2154 deferred.resolve(element); 2155 }); 2156 }; 2157 2158 if (endEventName) { 2159 element.bind(endEventName, transitionEndHandler); 2160 } 2161 2162 // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur 2163 $timeout(function() { 2164 if ( angular.isString(trigger) ) { 2165 element.addClass(trigger); 2166 } else if ( angular.isFunction(trigger) ) { 2167 trigger(element); 2168 } else if ( angular.isObject(trigger) ) { 2169 element.css(trigger); 2170 } 2171 //If browser does not support transitions, instantly resolve 2172 if ( !endEventName ) { 2173 deferred.resolve(element); 2174 } 2175 }); 2176 2177 // Add our custom cancel function to the promise that is returned 2178 // We can call this if we are about to run a new transition, which we know
2178will prevent this transition from ending, 2179 // i.e. it will therefore never raise a transitionEnd event for that transition 2180 deferred.promise.cancel = function() { 2181 if ( endEventName ) { 2182 element.unbind(endEventName, transitionEndHandler); 2183 } 2184 deferred.reject('Transition cancelled'); 2185 }; 2186 2187 return deferred.promise; 2188 }; 2189 2190 // Work out the name of the transitionEnd event 2191 var transElement = document.createElement('trans'); 2192 var transitionEndEventNames = { 2193 'WebkitTransition': 'webkitTransitionEnd', 2194 'MozTransition': 'transitionend', 2195 'OTransition': 'oTransitionEnd', 2196 'transition': 'transitionend' 2197 }; 2198 var animationEndEventNames = { 2199 'WebkitTransition': 'webkitAnimationEnd', 2200 'MozTransition': 'animationend', 2201 'OTransition': 'oAnimationEnd', 2202 'transition': 'animationend' 2203 }; 2204 function findEndEventName(endEventNames) { 2205 for (var name in endEventNames){ 2206 if (transElement.style[name] !== undefined) { 2207 return endEventNames[name]; 2208 } 2209 } 2210 } 2211 $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); 2212 $transition.animationEndEventName = findEndEventName(animationEndEventNames); 2213 return $transition; 2214}]); 2215 2216angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition']) 2217 2218// The collapsible directive indicates a block of html that will expand and collapse 2219.directive('collapse', ['$transition', function($transition) { 2220 // CSS transitions don't work with height: auto, so we have to manually change the height to a 2221 // specific value and then once the animation completes, we can reset the height to auto. 2222 // Unfortunately if you do this while the CSS transitions are specified (i.e. in the CSS class 2223 // "collapse") then you trigger a change to height 0 in between. 2224 // The fix is to remove the "collapse" CSS class while changing the height back to auto - phew! 2225 var fixUpHeight = function(scope, element, height) { 2226 // We remove the collapse CSS class to prevent a transition when we change to height: auto 2227 element.removeClass('collapse'); 2228 element.css({ height: height }); 2229 // It appears that reading offsetWidth makes the browser realise that we have changed the 2230 // height already :-/ 2231 var x = element[0].offsetWidth; 2232 element.addClass('collapse'); 2233 }; 2234 2235 return { 2236 link: function(scope, element, attrs) { 2237 2238 var isCollapsed; 2239 var initialAnimSkip = true; 2240 2241 scope.$watch(attrs.collapse, function(value) { 2242 if (value) { 2243 collapse(); 2244 } else { 2245 expand(); 2246 } 2247 }); 2248 2249 2250 var currentTransition; 2251 var doTransition = function(change) { 2252 if ( currentTransition ) { 2253 currentTransition.cancel(); 2254 } 2255 currentTransition = $transition(element,change); 2256 currentTransition.then( 2257 function() { currentTransition = undefined; }, 2258 function() { currentTransition = undefined; } 2259 ); 2260 return currentTransition; 2261 }; 2262 2263 var expand = function() { 2264 if (initialAnimSkip) { 2265 initialAnimSkip = false; 2266 if ( !isCollapsed ) { 2267 fixUpHeight(scope, element, 'auto'); 2268 element.addClass('in'); 2269 } 2270 } else { 2271 doTransition({ height : element[0].scrollHeight + 'px' }) 2272 .then(function() { 2273 // This check ensures that we don't accidentally update the height if the user has closed 2274 // the group while the animation was still running 2275 if ( !isCollapsed ) { 2276 fixUpHeight(scope, element, 'auto'); 2277 element.addClass('in'); 2278 } 2279 }); 2280 } 2281 isCollapsed = false; 2282 }; 2283 2284 var collapse = function() { 2285 isCollapsed = true; 2286 element.removeClass('in'); 2287 if (initialAnimSkip) { 2288 initialAnimSkip = false; 2289 fixUpHeight(scope, element, 0); 2290 } else { 2291 fixUpHeight(scope, element, element[0].scrollHeight + 'px'); 2292 doTransition({'height':'0'}); 2293 } 2294 }; 2295 } 2296 }; 2297}]); 2298 2299angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) 2300 2301.constant('accordionConfig', { 2302 closeOthers: true 2303}) 2304 2305.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { 2306 2307 // This array keeps track of the accordion groups 2308 this.groups = []; 2309 2310 // Keep reference to user's scope to properly assign `is-open` 2311 this.scope = $scope; 2312 2313 // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to 2314 this.closeOthers = function(openGroup) { 2315 var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; 2316 if ( closeOthers ) { 2317 angular.forEach(this.groups, function (group) { 2318 if ( group !== openGroup ) { 2319 group.isOpen = false;
2320 } 2321 }); 2322 } 2323 }; 2324 2325 // This is called from the accordion-group directive to add itself to the accordion 2326 this.addGroup = function(groupScope) { 2327 var that = this; 2328 this.groups.push(groupScope); 2329 2330 groupScope.$on('$destroy', function (event) { 2331 that.removeGroup(groupScope); 2332 }); 2333 }; 2334 2335 // This is called from the accordion-group directive when to remove itself 2336 this.removeGroup = function(group) { 2337 var index = this.groups.indexOf(group); 2338 if ( index !== -1 ) { 2339 this.groups.splice(this.groups.indexOf(group), 1); 2340 } 2341 }; 2342 2343}]) 2344 2345// The accordion directive simply sets up the directive controller 2346// and adds an accordion CSS class to itself element. 2347.directive('accordion', function () { 2348 return { 2349 restrict:'EA', 2350 controller:'AccordionController', 2351 transclude: true, 2352 replace: false, 2353 templateUrl: 'template/accordion/accordion.html' 2354 }; 2355}) 2356 2357// The accordion-group directive indicates a block of html that will expand and collapse in an accordion 2358.directive('accordionGroup', ['$parse', '$transition', '$timeout', function($parse, $transition, $timeout) { 2359 return { 2360 require:'^accordion', // We need this directive to be inside an accordion 2361 restrict:'EA', 2362 transclude:true, // It transcludes the contents of the directive into the template 2363 replace: true, // The element containing the directive will be replaced with the template 2364 templateUrl:'template/accordion/accordion-group.html', 2365 scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope 2366 controller: ['$scope', function($scope) { 2367 this.setHeading = function(element) { 2368 this.heading = element; 2369 }; 2370 }], 2371 link: function(scope, element, attrs, accordionCtrl) { 2372 var getIsOpen, setIsOpen; 2373 2374 accordionCtrl.addGroup(scope); 2375 2376 scope.isOpen = false; 2377 2378 if ( attrs.isOpen ) { 2379 getIsOpen = $parse(attrs.isOpen); 2380 setIsOpen = getIsOpen.assign; 2381 2382 accordionCtrl.scope.$watch(getIsOpen, function(value) { 2383 scope.isOpen = !!value; 2384 }); 2385 } 2386 2387 scope.$watch('isOpen', function(value) { 2388 if ( value ) { 2389 accordionCtrl.closeOthers(scope); 2390 } 2391 if ( setIsOpen ) { 2392 setIsOpen(accordionCtrl.scope, value); 2393 } 2394 }); 2395 } 2396 }; 2397}]) 2398 2399// Use accordion-heading below an accordion-group to provide a heading containing HTML 2400// <accordion-group> 2401// <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading> 2402// </accordion-group> 2403.directive('accordionHeading', function() { 2404 return { 2405 restrict: 'EA', 2406 transclude: true, // Grab the contents to be used as the heading 2407 template: '', // In effect remove this element! 2408 replace: true, 2409 require: '^accordionGroup', 2410 compile: function(element, attr, transclude) { 2411 return function link(scope, element, attr, accordionGroupCtrl) { 2412 // Pass the heading to the accordion-group controller 2413 // so that it can be transcluded into the right place in the template 2414 // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] 2415 accordionGroupCtrl.setHeading(transclude(scope, function() {})); 2416 }; 2417 } 2418 }; 2419}) 2420 2421// Use in the accordion-group template to indicate where you want the heading to be transcluded 2422// You must provide the property on the accordion-group controller that will hold the transcluded element 2423// <div class="accordion-group"> 2424// <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div> 2425// ... 2426// </div> 2427.directive('accordionTransclude', function() { 2428 return { 2429 require: '^accordionGroup', 2430 link: function(scope, element, attr, controller) { 2431 scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { 2432 if ( heading ) { 2433 element.html(''); 2434 element.append(heading); 2435 } 2436 }); 2437 } 2438 }; 2439}); 2440 2441angular.module("ui.bootstrap.alert", []).directive('alert', function () { 2442 return { 2443 restrict:'EA', 2444 templateUrl:'template/alert/alert.html', 2445 transclude:true, 2446 replace:true, 2447 scope: {
2448 type: '=', 2449 close: '&' 2450 }, 2451 link: function(scope, iElement, iAttrs) { 2452 scope.closeable = "close" in iAttrs; 2453 } 2454 }; 2455}); 2456 2457angular.module('ui.bootstrap.bindHtml', []) 2458 2459 .directive('bindHtmlUnsafe', function () { 2460 return function (scope, element, attr) { 2461 element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); 2462 scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { 2463 element.html(value || ''); 2464 }); 2465 }; 2466 }); 2467angular.module('ui.bootstrap.buttons', []) 2468 2469.constant('buttonConfig', { 2470 activeClass: 'active', 2471 toggleEvent: 'click' 2472}) 2473 2474.directive('btnRadio', ['buttonConfig', function (buttonConfig) { 2475 var activeClass = buttonConfig.activeClass || 'active'; 2476 var toggleEvent = buttonConfig.toggleEvent || 'click'; 2477 2478 return { 2479 require: 'ngModel', 2480 link: function (scope, element, attrs, ngModelCtrl) { 2481 2482 //model -> UI 2483 ngModelCtrl.$render = function () { 2484 element.toggleClass(activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); 2485 }; 2486 2487 //ui->model 2488 element.bind(toggleEvent, function () { 2489 if (!element.hasClass(activeClass)) { 2490 scope.$apply(function () { 2491 ngModelCtrl.$setViewValue(scope.$eval(attrs.btnRadio)); 2492 ngModelCtrl.$render(); 2493 }); 2494 } 2495 }); 2496 } 2497 }; 2498}]) 2499 2500.directive('btnCheckbox', ['buttonConfig', function (buttonConfig) { 2501 var activeClass = buttonConfig.activeClass || 'active'; 2502 var toggleEvent = buttonConfig.toggleEvent || 'click'; 2503 2504 return { 2505 require: 'ngModel', 2506 link: function (scope, element, attrs, ngModelCtrl) { 2507 2508 function getTrueValue() { 2509 var trueValue = scope.$eval(attrs.btnCheckboxTrue); 2510 return angular.isDefined(trueValue) ? trueValue : true; 2511 } 2512 2513 function getFalseValue() { 2514 var falseValue = scope.$eval(attrs.btnCheckboxFalse); 2515 return angular.isDefined(falseValue) ? falseValue : false; 2516 } 2517 2518 //model -> UI 2519 ngModelCtrl.$render = function () { 2520 element.toggleClass(activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); 2521 }; 2522 2523 //ui->model 2524 element.bind(toggleEvent, function () { 2525 scope.$apply(function () { 2526 ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? getFalseValue() : getTrueValue()); 2527 ngModelCtrl.$render(); 2528 }); 2529 }); 2530 } 2531 }; 2532}]); 2533 2534/** 2535* @ngdoc overview 2536* @name ui.bootstrap.carousel 2537* 2538* @description 2539* AngularJS version of an image carousel. 2540* 2541*/ 2542angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) 2543.controller('CarouselController', ['$scope', '$timeout', '$transition', '$q', function ($scope, $timeout, $transition, $q) { 2544 var self = this, 2545 slides = self.slides = [], 2546 currentIndex = -1, 2547 currentTimeout, isPlaying; 2548 self.currentSlide = null; 2549 2550 /* direction: "prev" or "next" */ 2551 self.select = function(nextSlide, direction) { 2552 var nextIndex = slides.indexOf(nextSlide); 2553 //Decide direction if it's not given 2554 if (direction === undefined) { 2555 direction = nextIndex > currentIndex ? "next" : "prev"; 2556 } 2557 if (nextSlide && nextSlide !== self.currentSlide) { 2558 if ($scope.$currentTransition) { 2559 $scope.$currentTransition.cancel(); 2560 //Timeout so ng-class in template has time to fix classes for finished slide 2561 $timeout(goNext); 2562 } else { 2563 goNext(); 2564 } 2565 } 2566 function goNext() { 2567 //If we have a slide to transition from and we have a transition type and we're allowed, go 2568 if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { 2569 //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime 2570 nextSlide.$element.addClass(direction); 2571 var reflow = nextSlide.$element[0].offsetWidth; //force reflow 2572 2573 //Set all other slides to stop doing their stuff for the new transition 2574 angular.forEach(slides, function(slide) { 2575 angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); 2576 }); 2577 angular.extend(nextSlide, {direction: direction, active: true, entering: true}); 2578 angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); 2579 2580 $scope.$currentTransition = $transition(nextSlide.$element, {}); 2581 //We have to create new pointers inside a closure since next & current will change 2582 (function(next,current) { 2583 $scope.$currentTransition.then( 2584 function(){ transitionDone(next, current); }, 2585 function(){ transitionDone(next, current); } 2586 ); 2587 }(nextSlide, self.currentSlide)); 2588 } else { 2589 transitionDone(nextSlide, self.currentSlide); 2590 } 2591 self.currentSlide = nextSlide; 2592 currentIndex = nextIndex; 2593 //every time you change slides, reset the timer 2594 restartTimer(); 2595 } 2596 function transitionDone(next, current) { 2597 angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); 2598 angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); 2599 $scope.$currentTransition = null; 2600 } 2601 }; 2602 2603 /* Allow outside people to call indexOf on slides array */ 2604 self.indexOfSlide = function(slide) { 2605 return slides.indexOf(slide); 2606 }; 2607 2608 $scope.next = function() { 2609 var newIndex = (currentIndex + 1) % slides.length; 2610 2611 //Prevent this user-triggered transition from occurring if there is already one in progress 2612 if (!$scope.$currentTransition) { 2613 return self.select(slides[newIndex], 'next'); 2614 } 2615 }; 2616 2617 $scope.prev = function() { 2618 var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; 2619 2620 //Prevent this user-triggered transition from occurring if there is already one in progress 2621 if (!$scope.$currentTransition) { 2622 return self.select(slides[newIndex], 'prev'); 2623 } 2624 }; 2625 2626 $scope.select = function(slide) { 2627 self.select(slide); 2628 }; 2629 2630 $scope.isActive = function(slide) { 2631 return self.currentSlide === slide; 2632 }; 2633 2634 $scope.slides = function() { 2635 return slides; 2636 }; 2637 2638 $scope.$watch('interval', restartTimer); 2639 function restartTimer() { 2640 if (currentTimeout) { 2641 $timeout.cancel(currentTimeout); 2642 } 2643 function go() { 2644 if (isPlaying) { 2645 $scope.next(); 2646 restartTimer(); 2647 } else { 2648 $scope.pause(); 2649 } 2650 } 2651 var interval = +$scope.interval; 2652 if (!isNaN(interval) && interval>=0) { 2653 currentTimeout = $timeout(go, interval); 2654 } 2655 } 2656 $scope.play = function() { 2657 if (!isPlaying) { 2658 isPlaying = true; 2659 restartTimer(); 2660 } 2661 }; 2662 $scope.pause = function() { 2663 if (!$scope.noPause) { 2664 isPlaying = false;
2665 if (currentTimeout) { 2666 $timeout.cancel(currentTimeout); 2667 } 2668 } 2669 }; 2670 2671 self.addSlide = function(slide, element) { 2672 slide.$element = element; 2673 slides.push(slide); 2674 //if this is the first slide or the slide is set to active, select it 2675 if(slides.length === 1 || slide.active) { 2676 self.select(slides[slides.length-1]); 2677 if (slides.length == 1) { 2678 $scope.play(); 2679 } 2680 } else { 2681 slide.active = false; 2682 } 2683 }; 2684 2685 self.removeSlide = function(slide) { 2686 //get the index of the slide inside the carousel 2687 var index = slides.indexOf(slide); 2688 slides.splice(index, 1); 2689 if (slides.length > 0 && slide.active) { 2690 if (index >= slides.length) { 2691 self.select(slides[index-1]); 2692 } else { 2693 self.select(slides[index]); 2694 } 2695 } else if (currentIndex > index) { 2696 currentIndex--; 2697 } 2698 }; 2699}]) 2700 2701/** 2702 * @ngdoc directive 2703 * @name ui.bootstrap.carousel.directive:carousel 2704 * @restrict EA 2705 * 2706 * @description 2707 * Carousel is the outer container for a set of image 'slides' to showcase. 2708 * 2709 * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. 2710 * @param {boolean=} noTransition Whether to disable transitions on the carousel. 2711 * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). 2712 * 2713 * @example 2714<example module="ui.bootstrap"> 2715 <file name="index.html"> 2716 <carousel> 2717 <slide> 2718 <img src="http://placekitten.com/150/150" style="margin:auto;"> 2719 <div class="carousel-caption"> 2720 <p>Beautiful!</p> 2721 </div> 2722 </slide> 2723 <slide> 2724 <img src="http://placekitten.com/100/150" style="margin:auto;"> 2725 <div class="carousel-caption"> 2726 <p>D'aww!</p> 2727 </div> 2728 </slide> 2729 </carousel> 2730 </file> 2731 <file name="demo.css"> 2732 .carousel-indicators { 2733 top: auto; 2734 bottom: 15px; 2735 } 2736 </file> 2737</example> 2738 */ 2739.directive('carousel', [function() { 2740 return { 2741 restrict: 'EA', 2742 transclude: true, 2743 replace: true, 2744 controller: 'CarouselController', 2745 require: 'carousel', 2746 templateUrl: 'template/carousel/carousel.html', 2747 scope: { 2748 interval: '=', 2749 noTransition: '=', 2750 noPause: '=' 2751 } 2752 }; 2753}]) 2754 2755/** 2756 * @ngdoc directive 2757 * @name ui.bootstrap.carousel.directive:slide 2758 * @restrict EA 2759 * 2760 * @description 2761 * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. 2762 * 2763 * @param {boolean=} active Model binding, whether or not this slide is currently active. 2764 * 2765 * @example 2766<example module="ui.bootstrap"> 2767 <file name="index.html"> 2768<div ng-controller="CarouselDemoCtrl"> 2769 <carousel> 2770 <slide ng-repeat="slide in slides" active="slide.active"> 2771 <img ng-src="{{slide.image}}" style="margin:auto;"> 2772 <div class="carousel-caption"> 2773 <h4>Slide {{$index}}</h4> 2774 <p>{{slide.text}}</p> 2775 </div> 2776 </slide> 2777 </carousel> 2778 <div class="row-fluid"> 2779 <div class="span6"> 2780 <ul> 2781 <li ng-repeat="slide in slides"> 2782 <button class="btn btn-mini" ng-class="{'btn-info': !slide.active, 'btn-success': slide.active}" ng-disabled="slide.active" ng-click="slide.active = true">select</button> 2783 {{$index}}: {{slide.text}} 2784 </li> 2785 </ul> 2786 <a class="btn" ng-click="addSlide()">Add Slide</a> 2787 </div> 2788 <div class="span6"> 2789 Interval, in milliseconds: <input type="number" ng-model="myInterval"> 2790 <br />Enter a negative number to stop the interval. 2791 </div> 2792 </div> 2793</div> 2794 </file> 2795 <file name="script.js"> 2796function CarouselDemoCtrl($scope) { 2797 $scope.myInterval = 5000; 2798 var slides = $scope.slides = []; 2799 $scope.addSlide = function() { 2800 var newWidth = 200 + ((slides.length + (25 * slides.length)) % 150); 2801 slides.push({ 2802 image: 'http://placekitten.com/' + newWidth + '/200', 2803 text: ['More','Extra','Lots of','Surplus'][slides.length % 4] + ' ' 2804 ['Cats', 'Kittys', 'Felines', 'Cutes'][slides.length % 4] 2805 }); 2806 }; 2807 for (var i=0; i<4; i++) $scope.addSlide(); 2808} 2809 </file> 2810 <file name="demo.css"> 2811 .carousel-indicators { 2812 top: auto; 2813 bottom: 15px; 2814 } 2815 </file> 2816</example> 2817*/ 2818 2819.directive('slide', ['$parse', function($parse) { 2820 return { 2821 require: '^carousel', 2822 restrict: 'EA', 2823 transclude: true, 2824 replace: true, 2825 templateUrl: 'template/carousel/slide.html', 2826 scope: { 2827 }, 2828 link: function (scope, element, attrs, carouselCtrl) { 2829 //Set up optional 'active' = binding 2830 if (attrs.active) { 2831 var getActive = $parse(attrs.active); 2832 var setActive = getActive.assign; 2833 var lastValue = scope.active = getActive(scope.$parent); 2834 scope.$watch(function parentActiveWatch() { 2835 var parentActive = getActive(scope.$parent); 2836 2837 if (parentActive !== scope.active) { 2838 // we are out of sync and need to copy 2839 if (parentActive !== lastValue) { 2840 // parent changed and it has precedence
2841 lastValue = scope.active = parentActive; 2842 } else { 2843 // if the parent can be assigned then do so 2844 setActive(scope.$parent, parentActive = lastValue = scope.active); 2845 } 2846 } 2847 return parentActive; 2848 }); 2849 } 2850 2851 carouselCtrl.addSlide(scope, element); 2852 //when the scope is destroyed then remove the slide from the current slides array 2853 scope.$on('$destroy', function() { 2854 carouselCtrl.removeSlide(scope); 2855 }); 2856 2857 scope.$watch('active', function(active) { 2858 if (active) { 2859 carouselCtrl.select(scope); 2860 } 2861 }); 2862 } 2863 }; 2864}]); 2865 2866angular.module('ui.bootstrap.position', []) 2867 2868/** 2869 * A set of utility methods that can be use to retrieve position of DOM elements. 2870 * It is meant to be used where we need to absolute-position DOM elements in 2871 * relation to other, existing elements (this is the case for tooltips, popovers, 2872 * typeahead suggestions etc.). 2873 */ 2874 .factory('$position', ['$document', '$window', function ($document, $window) { 2875 2876 function getStyle(el, cssprop) { 2877 if (el.currentStyle) { //IE 2878 return el.currentStyle[cssprop]; 2879 } else if ($window.getComputedStyle) { 2880 return $window.getComputedStyle(el)[cssprop]; 2881 } 2882 // finally try and get inline style 2883 return el.style[cssprop]; 2884 } 2885 2886 /** 2887 * Checks if a given element is statically positioned 2888 * @param element - raw DOM element 2889 */ 2890 function isStaticPositioned(element) { 2891 return (getStyle(element, "position") || 'static' ) === 'static'; 2892 } 2893 2894 /** 2895 * returns the closest, non-statically positioned parentOffset of a given element 2896 * @param element 2897 */ 2898 var parentOffsetEl = function (element) { 2899 var docDomEl = $document[0]; 2900 var offsetParent = element.offsetParent || docDomEl; 2901 while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { 2902 offsetParent = offsetParent.offsetParent; 2903 } 2904 return offsetParent || docDomEl; 2905 }; 2906 2907 return { 2908 /** 2909 * Provides read-only equivalent of jQuery's position function: 2910 * http://api.jquery.com/position/ 2911 */ 2912 position: function (element) { 2913 var elBCR = this.offset(element); 2914 var offsetParentBCR = { top: 0, left: 0 }; 2915 var offsetParentEl = parentOffsetEl(element[0]); 2916 if (offsetParentEl != $document[0]) { 2917 offsetParentBCR = this.offset(angular.element(offsetParentEl)); 2918 offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; 2919 offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; 2920 } 2921 2922 var boundingClientRect = element[0].getBoundingClientRect(); 2923 return { 2924 width: boundingClientRect.width || element.prop('offsetWidth'), 2925 height: boundingClientRect.height || element.prop('offsetHeight'), 2926 top: elBCR.top - offsetParentBCR.top, 2927 left: elBCR.left - offsetParentBCR.left 2928 }; 2929 }, 2930 2931 /** 2932 * Provides read-only equivalent of jQuery's offset function: 2933 * http://api.jquery.com/offset/ 2934 */ 2935 offset: function (element) { 2936 var boundingClientRect = element[0].getBoundingClientRect(); 2937 return { 2938 width: boundingClientRect.width || element.prop('offsetWidth'), 2939 height: boundingClientRect.height || element.prop('offsetHeight'), 2940 top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop), 2941 left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft) 2942 }; 2943 } 2944 }; 2945 }]); 2946 2947angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) 2948 2949.constant('datepickerConfig', { 2950 dayFormat: 'dd', 2951 monthFormat: 'MMMM', 2952 yearFormat: 'yyyy', 2953 dayHeaderFormat: 'EEE', 2954 dayTitleFormat: 'MMMM yyyy', 2955 monthTitleFormat: 'yyyy', 2956 showWeeks: true, 2957 startingDay: 0, 2958 yearRange: 20, 2959 minDate: null, 2960 maxDate: null 2961}) 2962 2963.controller('DatepickerController', ['$scope', '$attrs', 'dateFilter', 'datepickerConfig', function($scope, $attrs, dateFilter, dtConfig) { 2964 var format = { 2965 day: getValue($attrs.dayFormat, dtConfig.dayFormat), 2966 month: getValue($attrs.monthFormat, dtConfig.monthFormat), 2967 year: getValue($attrs.yearFormat, dtConfig.yearFormat), 2968 dayHeader: getValue($attrs.dayHeaderFormat, dtConfig.dayHeaderFormat), 2969 dayTitle: getValue($attrs.dayTitleFormat, dtConfig.dayTitleFormat), 2970 monthTitle: getValue($attrs.monthTitleFormat, dtConfig.monthTitleFormat) 2971 }, 2972 startingDay = getValue($attrs.startingDay, dtConfig.startingDay), 2973 yearRange = getValue($attrs.yearRange, dtConfig.yearRange); 2974 2975 this.minDate = dtConfig.minDate ? new Date(dtConfig.minDate) : null; 2976 this.maxDate = dtConfig.maxDate ? new Date(dtConfig.maxDate) : null; 2977 2978 function getValue(value, defaultValue) { 2979 return angular.isDefined(value) ? $scope.$parent.$eval(value) : defaultValue; 2980 } 2981 2982 function getDaysInMonth( year, month ) { 2983 return new Date(year, month, 0).getDate(); 2984 } 2985 2986 function getDates(startDate, n) { 2987 var dates = new Array(n); 2988 var current = startDate, i = 0; 2989 while (i < n) { 2990 dates[i++] = new Date(current); 2991 current.setDate( current.getDate() + 1 ); 2992 } 2993 return dates; 2994 } 2995 2996 function makeDate(date, format, isSelected, isSecondary) { 2997 return { date: date, label: dateFilter(date, format), selected: !!isSelected, secondary: !!isSecondary }; 2998 } 2999 3000 this.modes = [ 3001 { 3002 name: 'day', 3003 getVisibleDates: function(date, selected) {
3004 var year = date.getFullYear(), month = date.getMonth(), firstDayOfMonth = new Date(year, month, 1); 3005 var difference = startingDay - firstDayOfMonth.getDay(), 3006 numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, 3007 firstDate = new Date(firstDayOfMonth), numDates = 0; 3008 3009 if ( numDisplayedFromPreviousMonth > 0 ) { 3010 firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); 3011 numDates += numDisplayedFromPreviousMonth; // Previous 3012 } 3013 numDates += getDaysInMonth(year, month + 1); // Current 3014 numDates += (7 - numDates % 7) % 7; // Next 3015 3016 var days = getDates(firstDate, numDates), labels = new Array(7); 3017 for (var i = 0; i < numDates; i ++) { 3018 var dt = new Date(days[i]); 3019 days[i] = makeDate(dt, format.day, (selected && selected.getDate() === dt.getDate() && selected.getMonth() === dt.getMonth() && selected.getFullYear() === dt.getFullYear()), dt.getMonth() !== month); 3020 } 3021 for (var j = 0; j < 7; j++) { 3022 labels[j] = dateFilter(days[j].date, format.dayHeader); 3023 } 3024 return { objects: days, title: dateFilter(date, format.dayTitle), labels: labels }; 3025 }, 3026 compare: function(date1, date2) { 3027 return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); 3028 }, 3029 split: 7, 3030 step: { months: 1 } 3031 }, 3032 { 3033 name: 'month', 3034 getVisibleDates: function(date, selected) { 3035 var months = new Array(12), year = date.getFullYear(); 3036 for ( var i = 0; i < 12; i++ ) { 3037 var dt = new Date(year, i, 1); 3038 months[i] = makeDate(dt, format.month, (selected && selected.getMonth() === i && selected.getFullYear() === year)); 3039 } 3040 return { objects: months, title: dateFilter(date, format.monthTitle) }; 3041 }, 3042 compare: function(date1, date2) { 3043 return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); 3044 }, 3045 split: 3, 3046 step: { years: 1 } 3047 }, 3048 { 3049 name: 'year', 3050 getVisibleDates: function(date, selected) { 3051 var years = new Array(yearRange), year = date.getFullYear(), startYear = parseInt((year - 1) / yearRange, 10) * yearRange + 1; 3052 for ( var i = 0; i < yearRange; i++ ) { 3053 var dt = new Date(startYear + i, 0, 1); 3054 years[i] = makeDate(dt, format.year, (selected && selected.getFullYear() === dt.getFullYear())); 3055 } 3056 return { objects: years, title: [years[0].label, years[yearRange - 1].label].join(' - ') }; 3057 }, 3058 compare: function(date1, date2) { 3059 return date1.getFullYear() - date2.getFullYear(); 3060 }, 3061 split: 5, 3062 step: { years: yearRange } 3063 } 3064 ]; 3065 3066 this.isDisabled = function(date, mode) { 3067 var currentMode = this.modes[mode || 0]; 3068 return ((this.minDate && currentMode.compare(date, this.minDate) < 0) || (this.maxDate && currentMode.compare(date, this.maxDate) > 0) || ($scope.dateDisabled && $scope.dateDisabled({date: date, mode: currentMode.name}))); 3069 }; 3070}]) 3071 3072.directive( 'datepicker', ['dateFilter', '$parse', 'datepickerConfig', '$log', function (dateFilter, $parse, datepickerConfig, $log) { 3073 return { 3074 restrict: 'EA', 3075 replace: true, 3076 templateUrl: 'template/datepicker/datepicker.html', 3077 scope: { 3078 dateDisabled: '&' 3079 }, 3080 require: ['datepicker', '?^ngModel'], 3081 controller: 'DatepickerController', 3082 link: function(scope, element, attrs, ctrls) { 3083 var datepickerCtrl = ctrls[0], ngModel = ctrls[1]; 3084 3085 if (!ngModel) { 3086 return; // do nothing if no ng-model 3087 } 3088 3089 // Configuration parameters 3090 var mode = 0, selected = new Date(), showWeeks = datepickerConfig.showWeeks; 3091 3092 if (attrs.showWeeks) { 3093 scope.$parent.$watch($parse(attrs.showWeeks), function(value) { 3094 showWeeks = !! value; 3095 updateShowWeekNumbers(); 3096 }); 3097 } else { 3098 updateShowWeekNumbers(); 3099 } 3100 3101 if (attrs.min) { 3102 scope.$parent.$watch($parse(attrs.min), function(value) { 3103 datepickerCtrl.minDate = value ? new Date(value) : null; 3104 refill(); 3105 }); 3106 } 3107 if (attrs.max) { 3108 scope.$parent.$watch($parse(attrs.max), function(value) { 3109 datepickerCtrl.maxDate = value ? new Date(value) : null; 3110 refill(); 3111 }); 3112 } 3113 3114 function updateShowWeekNumbers() { 3115 scope.showWeekNumbers = mode === 0 && showWeeks; 3116 } 3117 3118 // Split array into smaller arrays 3119 function split(arr, size) { 3120 var arrays = []; 3121 while (arr.length > 0) { 3122 arrays.push(arr.splice(0, size)); 3123 } 3124 return arrays; 3125 } 3126 3127 function refill( updateSelected ) { 3128 var date = null, valid = true; 3129 3130 if ( ngModel.$modelValue ) { 3131 date = new Date( ngModel.$modelValue ); 3132 3133 if ( isNaN(date) ) { 3134 valid = false;
3135 $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); 3136 } else if ( updateSelected ) { 3137 selected = date; 3138 } 3139 } 3140 ngModel.$setValidity('date', valid); 3141 3142 var currentMode = datepickerCtrl.modes[mode], data = currentMode.getVisibleDates(selected, date); 3143 angular.forEach(data.objects, function(obj) { 3144 obj.disabled = datepickerCtrl.isDisabled(obj.date, mode); 3145 }); 3146 3147 ngModel.$setValidity('date-disabled', (!date || !datepickerCtrl.isDisabled(date))); 3148 3149 scope.rows = split(data.objects, currentMode.split); 3150 scope.labels = data.labels || []; 3151 scope.title = data.title; 3152 } 3153 3154 function setMode(value) { 3155 mode = value; 3156 updateShowWeekNumbers(); 3157 refill(); 3158 } 3159 3160 ngModel.$render = function() { 3161 refill( true ); 3162 }; 3163 3164 scope.select = function( date ) { 3165 if ( mode === 0 ) { 3166 var dt = new Date( ngModel.$modelValue ); 3167 dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); 3168 ngModel.$setViewValue( dt ); 3169 refill( true ); 3170 } else { 3171 selected = date; 3172 setMode( mode - 1 ); 3173 } 3174 }; 3175 scope.move = function(direction) { 3176 var step = datepickerCtrl.modes[mode].step; 3177 selected.setMonth( selected.getMonth() + direction * (step.months || 0) ); 3178 selected.setFullYear( selected.getFullYear() + direction * (step.years || 0) ); 3179 refill(); 3180 }; 3181 scope.toggleMode = function() { 3182 setMode( (mode + 1) % datepickerCtrl.modes.length ); 3183 }; 3184 scope.getWeekNumber = function(row) { 3185 return ( mode === 0 && scope.showWeekNumbers && row.length === 7 ) ? getISO8601WeekNumber(row[0].date) : null; 3186 }; 3187 3188 function getISO8601WeekNumber(date) { 3189 var checkDate = new Date(date); 3190 checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday 3191 var time = checkDate.getTime(); 3192 checkDate.setMonth(0); // Compare with Jan 1 3193 checkDate.setDate(1); 3194 return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; 3195 } 3196 } 3197 }; 3198}]) 3199 3200.constant('datepickerPopupConfig', { 3201 dateFormat: 'yyyy-MM-dd', 3202 currentText: 'Today', 3203 toggleWeeksText: 'Weeks', 3204 clearText: 'Clear', 3205 closeText: 'Done', 3206 closeOnDateSelection: true, 3207 appendToBody: false 3208}) 3209 3210.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'datepickerPopupConfig', 'datepickerConfig', 3211function ($compile, $parse, $document, $position, dateFilter, datepickerPopupConfig, datepickerConfig) { 3212 return { 3213 restrict: 'EA', 3214 require: 'ngModel', 3215 link: function(originalScope, element, attrs, ngModel) { 3216 var dateFormat; 3217 attrs.$observe('datepickerPopup', function(value) { 3218 dateFormat = value || datepickerPopupConfig.dateFormat; 3219 ngModel.$render(); 3220 }); 3221 3222 var closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? originalScope.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection; 3223 var appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? originalScope.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; 3224 3225 // create a child scope for the datepicker directive so we are not polluting original scope 3226 var scope = originalScope.$new(); 3227 3228 originalScope.$on('$destroy', function() { 3229 scope.$destroy(); 3230 }); 3231 3232 attrs.$observe('currentText', function(text) { 3233 scope.currentText = angular.isDefined(text) ? text : datepickerPopupConfig.currentText; 3234 }); 3235 attrs.$observe('toggleWeeksText', function(text) { 3236 scope.toggleWeeksText = angular.isDefined(text) ? text : datepickerPopupConfig.toggleWeeksText; 3237 }); 3238 attrs.$observe('clearText', function(text) { 3239 scope.clearText = angular.isDefined(text) ? text : datepickerPopupConfig.clearText; 3240 }); 3241 attrs.$observe('closeText', function(text) { 3242 scope.closeText = angular.isDefined(text) ? text : datepickerPopupConfig.closeText; 3243 }); 3244 3245 var getIsOpen, setIsOpen; 3246 if ( attrs.isOpen ) { 3247 getIsOpen = $parse(attrs.isOpen); 3248 setIsOpen = getIsOpen.assign; 3249 3250 originalScope.$watch(getIsOpen, function updateOpen(value) { 3251 scope.isOpen = !! value; 3252 }); 3253 } 3254 scope.isOpen = getIsOpen ? getIsOpen(originalScope) : false; // Initial state 3255 3256 function setOpen( value ) { 3257 if (setIsOpen) { 3258 setIsOpen(originalScope, !!value); 3259 } else { 3260 scope.isOpen = !!value; 3261 } 3262 } 3263 3264 var documentClickBind = function(event) { 3265 if (scope.isOpen && event.target !== element[0]) { 3266 scope.$apply(function() { 3267 setOpen(false); 3268 }); 3269 } 3270 }; 3271 3272 var elementFocusBind = function() { 3273 scope.$apply(function() { 3274 setOpen( true ); 3275 }); 3276 }; 3277 3278 // popup element used to display calendar 3279 var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>'); 3280 popupEl.attr({ 3281 'ng-model': 'date', 3282 'ng-change': 'dateSelection()' 3283 }); 3284 var datepickerEl = angular.element(popupEl.children()[0]); 3285 if (attrs.datepickerOptions) { 3286 datepickerEl.attr(angular.extend({}, originalScope.$eval(attrs.datepickerOptions))); 3287 } 3288 3289 // TODO: reverse from dateFilter string to Date object 3290 function parseDate(viewValue) { 3291 if (!viewValue) { 3292 ngModel.$setValidity('date', true); 3293 return null; 3294 } else if (angular.isDate(viewValue)) { 3295 ngModel.$setValidity('date', true); 3296 return viewValue; 3297 } else if (angular.isString(viewValue)) { 3298 var date = new Date(viewValue); 3299 if (isNaN(date)) { 3300 ngModel.$setValidity('date', false); 3301 return undefined; 3302 } else { 3303 ngModel.$setValidity('date', true); 3304 return date; 3305 } 3306 } else { 3307 ngModel.$setValidity('date', false); 3308 return undefined; 3309 } 3310 } 3311 ngModel.$parsers.unshift(parseDate); 3312 3313 // Inner change 3314 scope.dateSelection = function() { 3315 ngModel.$setViewValue(scope.date); 3316 ngModel.$render(); 3317 3318 if (closeOnDateSelection) {
3319 setOpen( false ); 3320 } 3321 }; 3322 3323 element.bind('input change keyup', function() { 3324 scope.$apply(function() { 3325 updateCalendar(); 3326 }); 3327 }); 3328 3329 // Outter change 3330 ngModel.$render = function() { 3331 var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : ''; 3332 element.val(date); 3333 3334 updateCalendar(); 3335 }; 3336 3337 function updateCalendar() { 3338 scope.date = ngModel.$modelValue; 3339 updatePosition(); 3340 } 3341 3342 function addWatchableAttribute(attribute, scopeProperty, datepickerAttribute) { 3343 if (attribute) { 3344 originalScope.$watch($parse(attribute), function(value){ 3345 scope[scopeProperty] = value; 3346 }); 3347 datepickerEl.attr(datepickerAttribute || scopeProperty, scopeProperty); 3348 } 3349 } 3350 addWatchableAttribute(attrs.min, 'min'); 3351 addWatchableAttribute(attrs.max, 'max'); 3352 if (attrs.showWeeks) { 3353 addWatchableAttribute(attrs.showWeeks, 'showWeeks', 'show-weeks'); 3354 } else { 3355 scope.showWeeks = datepickerConfig.showWeeks; 3356 datepickerEl.attr('show-weeks', 'showWeeks'); 3357 } 3358 if (attrs.dateDisabled) { 3359 datepickerEl.attr('date-disabled', attrs.dateDisabled); 3360 } 3361 3362 function updatePosition() { 3363 scope.position = appendToBody ? $position.offset(element) : $position.position(element); 3364 scope.position.top = scope.position.top + element.prop('offsetHeight'); 3365 } 3366 3367 var documentBindingInitialized = false, elementFocusInitialized = false; 3368 scope.$watch('isOpen', function(value) { 3369 if (value) { 3370 updatePosition(); 3371 $document.bind('click', documentClickBind); 3372 if(elementFocusInitialized) { 3373 element.unbind('focus', elementFocusBind); 3374 } 3375 element[0].focus(); 3376 documentBindingInitialized = true; 3377 } else { 3378 if(documentBindingInitialized) { 3379 $document.unbind('click', documentClickBind); 3380 } 3381 element.bind('focus', elementFocusBind); 3382 elementFocusInitialized = true; 3383 } 3384 3385 if ( setIsOpen ) { 3386 setIsOpen(originalScope, value); 3387 } 3388 }); 3389 3390 var $setModelValue = $parse(attrs.ngModel).assign; 3391 3392 scope.today = function() { 3393 $setModelValue(originalScope, new Date()); 3394 }; 3395 scope.clear = function() { 3396 $setModelValue(originalScope, null); 3397 }; 3398 3399 var $popup = $compile(popupEl)(scope); 3400 if ( appendToBody ) { 3401 $document.find('body').append($popup); 3402 } else { 3403 element.after($popup); 3404 } 3405 } 3406 }; 3407}]) 3408 3409.directive('datepickerPopupWrap', function() { 3410 return { 3411 restrict:'EA', 3412 replace: true, 3413 transclude: true, 3414 templateUrl: 'template/datepicker/popup.html', 3415 link:function (scope, element, attrs) { 3416 element.bind('click', function(event) { 3417 event.preventDefault(); 3418 event.stopPropagation(); 3419 }); 3420 } 3421 }; 3422}); 3423 3424/* 3425 * dropdownToggle - Provides dropdown menu functionality in place of bootstrap js 3426 * @restrict class or attribute 3427 * @example: 3428 <li class="dropdown"> 3429 <a class="dropdown-toggle">My Dropdown Menu</a> 3430 <ul class="dropdown-menu"> 3431 <li ng-repeat="choice in dropChoices"> 3432 <a ng-href="{{choice.href}}">{{choice.text}}</a> 3433 </li> 3434 </ul> 3435 </li> 3436 */ 3437 3438angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', ['$document', '$location', function ($document, $location) { 3439 var openElement = null, 3440 closeMenu = angular.noop; 3441 return { 3442 restrict: 'CA', 3443 link: function(scope, element, attrs) { 3444 scope.$watch('$location.path', function() { closeMenu(); }); 3445 element.parent().bind('click', function() { closeMenu(); }); 3446 element.bind('click', function (event) { 3447 3448 var elementWasOpen = (element === openElement); 3449 3450 event.preventDefault(); 3451 event.stopPropagation(); 3452 3453 if (!!openElement) { 3454 closeMenu(); 3455 } 3456 3457 if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) { 3458 element.parent().addClass('open'); 3459 openElement = element; 3460 closeMenu = function (event) { 3461 if (event) { 3462 event.preventDefault(); 3463 event.stopPropagation(); 3464 } 3465 $document.unbind('click', closeMenu); 3466 element.parent().removeClass('open'); 3467 closeMenu = angular.noop; 3468 openElement = null; 3469 }; 3470 $document.bind('click', closeMenu); 3471 } 3472 }); 3473 } 3474 }; 3475}]); 3476 3477angular.module('ui.bootstrap.modal', []) 3478 3479/** 3480 * A helper, internal data structure that acts as a map but also allows getting / removing 3481 * elements in the LIFO order 3482 */ 3483 .factory('$$stackedMap', function () { 3484 return { 3485 createNew: function () { 3486 var stack = []; 3487 3488 return { 3489 add: function (key, value) { 3490 stack.push({ 3491 key: key, 3492 value: value 3493 }); 3494 }, 3495 get: function (key) { 3496 for (var i = 0; i < stack.length; i++) { 3497 if (key == stack[i].key) { 3498 return stack[i]; 3499 } 3500 } 3501 }, 3502 keys: function() { 3503 var keys = []; 3504 for (var i = 0; i < stack.length; i++) { 3505 keys.push(stack[i].key); 3506 } 3507 return keys; 3508 }, 3509 top: function () { 3510 return stack[stack.length - 1]; 3511 }, 3512 remove: function (key) { 3513 var idx = -1; 3514 for (var i = 0; i < stack.length; i++) { 3515 if (key == stack[i].key) { 3516 idx = i; 3517 break; 3518 } 3519 } 3520 return stack.splice(idx, 1)[0]; 3521 }, 3522 removeTop: function () { 3523 return stack.splice(stack.length - 1, 1)[0]; 3524 }, 3525 length: function () { 3526 return stack.length; 3527 } 3528 }; 3529 } 3530 }; 3531 }) 3532 3533/** 3534 * A helper directive for the $modal service. It creates a backdrop element. 3535 */ 3536 .directive('modalBackdrop', ['$modalStack', '$timeout', function ($modalStack, $timeout) { 3537 return { 3538 restrict: 'EA', 3539 replace: true, 3540 templateUrl: 'template/modal/backdrop.html', 3541 link: function (scope, element, attrs) { 3542 3543 //trigger CSS transitions 3544 $timeout(function () { 3545 scope.animate = true; 3546 }); 3547 3548 scope.close = function (evt) { 3549 var modal = $modalStack.getTop(); 3550 if (modal && modal.value.backdrop && modal.value.backdrop != 'static') { 3551 evt.preventDefault(); 3552 evt.stopPropagation(); 3553 $modalStack.dismiss(modal.key, 'backdrop click'); 3554 } 3555 }; 3556 } 3557 }; 3558 }]) 3559 3560 .directive('modalWindow', ['$timeout', function ($timeout) { 3561 return { 3562 restrict: 'EA', 3563 scope: { 3564 index: '@' 3565 }, 3566 replace: true, 3567 transclude: true, 3568 templateUrl: 'template/modal/window.html', 3569 link: function (scope, element, attrs) { 3570 scope.windowClass = attrs.windowClass || ''; 3571 3572 //trigger CSS transitions 3573 $timeout(function () { 3574 scope.animate = true; 3575 }); 3576 } 3577 }; 3578 }]) 3579 3580 .factory('$modalStack', ['$document', '$compile', '$rootScope', '$$stackedMap', 3581 function ($document, $compile, $rootScope, $$stackedMap) { 3582 3583 var backdropjqLiteEl, backdropDomEl; 3584 var backdropScope = $rootScope.$new(true); 3585 var body = $document.find('body').eq(0); 3586 var openedWindows = $$stackedMap.createNew(); 3587 var $modalStack = {}; 3588 3589 function backdropIndex() { 3590 var topBackdropIndex = -1; 3591 var opened = openedWindows.keys(); 3592 for (var i = 0; i < opened.length; i++) { 3593 if (openedWindows.get(opened[i]).value.backdrop) { 3594 topBackdropIndex = i; 3595 } 3596 } 3597 return topBackdropIndex; 3598 } 3599 3600 $rootScope.$watch(backdropIndex, function(newBackdropIndex){ 3601 backdropScope.index = newBackdropIndex; 3602 }); 3603 3604 function removeModalWindow(modalInstance) { 3605 3606 var modalWindow = openedWindows.get(modalInstance).value; 3607 3608 //clean up the stack 3609 openedWindows.remove(modalInstance); 3610 3611 //remove window DOM element 3612 modalWindow.modalDomEl.remove(); 3613 3614 //remove backdrop if no longer needed 3615 if (backdropDomEl && backdropIndex() == -1) { 3616 backdropDomEl.remove(); 3617 backdropDomEl = undefined; 3618 } 3619 3620 //destroy scope 3621 modalWindow.modalScope.$destroy(); 3622 } 3623 3624 $document.bind('keydown', function (evt) { 3625 var modal; 3626 3627 if (evt.which === 27) { 3628 modal = openedWindows.top(); 3629 if (modal && modal.value.keyboard) { 3630 $rootScope.$apply(function () { 3631 $modalStack.dismiss(modal.key); 3632 }); 3633 } 3634 } 3635 }); 3636 3637 $modalStack.open = function (modalInstance, modal) { 3638 3639 openedWindows.add(modalInstance, { 3640 deferred: modal.deferred, 3641 modalScope: modal.scope, 3642 backdrop: modal.backdrop, 3643 keyboard: modal.keyboard 3644 }); 3645 3646 var angularDomEl = angular.element('<div modal-window></div>'); 3647 angularDomEl.attr('window-class', modal.windowClass); 3648 angularDomEl.attr('index', openedWindows.length() - 1); 3649 angularDomEl.html(modal.content); 3650 3651 var modalDomEl = $compile(angularDomEl)(modal.scope); 3652 openedWindows.top().value.modalDomEl = modalDomEl; 3653 body.append(modalDomEl); 3654 3655 if (backdropIndex() >= 0 && !backdropDomEl) { 3656 backdropjqLiteEl = angular.element('<div modal-backdrop></div>'); 3657 backdropDomEl = $compile(backdropjqLiteEl)(backdropScope); 3658 body.append(backdropDomEl); 3659 } 3660 }; 3661 3662 $modalStack.close = function (modalInstance, result) { 3663 var modal = openedWindows.get(modalInstance); 3664 if (modal) { 3665 modal.value.deferred.resolve(result); 3666 removeModalWindow(modalInstance); 3667 } 3668 }; 3669 3670 $modalStack.dismiss = function (modalInstance, reason) { 3671 var modalWindow = openedWindows.get(modalInstance).value; 3672 if (modalWindow) { 3673 modalWindow.deferred.reject(reason); 3674 removeModalWindow(modalInstance); 3675 } 3676 }; 3677 3678 $modalStack.getTop = function () { 3679 return openedWindows.top(); 3680 }; 3681 3682 return $modalStack; 3683 }]) 3684 3685 .provider('$modal', function () { 3686 3687 var $modalProvider = { 3688 options: { 3689 backdrop: true, //can be also false or 'static' 3690 keyboard: true 3691 }, 3692 $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', 3693 function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { 3694 3695 var $modal = {}; 3696 3697 function getTemplatePromise(options) { 3698 return options.template ? $q.when(options.template) : 3699 $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) { 3700 return result.data; 3701 }); 3702 } 3703 3704 function getResolvePromises(resolves) { 3705 var promisesArr = []; 3706 angular.forEach(resolves, function (value, key) { 3707 if (angular.isFunction(value) || angular.isArray(value)) {
3708 promisesArr.push($q.when($injector.invoke(value))); 3709 } 3710 }); 3711 return promisesArr; 3712 } 3713 3714 $modal.open = function (modalOptions) { 3715 3716 var modalResultDeferred = $q.defer(); 3717 var modalOpenedDeferred = $q.defer(); 3718 3719 //prepare an instance of a modal to be injected into controllers and returned to a caller 3720 var modalInstance = { 3721 result: modalResultDeferred.promise, 3722 opened: modalOpenedDeferred.promise, 3723 close: function (result) { 3724 $modalStack.close(modalInstance, result); 3725 }, 3726 dismiss: function (reason) { 3727 $modalStack.dismiss(modalInstance, reason); 3728 } 3729 }; 3730 3731 //merge and clean up options 3732 modalOptions = angular.extend({}, $modalProvider.options, modalOptions); 3733 modalOptions.resolve = modalOptions.resolve || {}; 3734 3735 //verify options 3736 if (!modalOptions.template && !modalOptions.templateUrl) { 3737 throw new Error('One of template or templateUrl options is required.'); 3738 } 3739 3740 var templateAndResolvePromise = 3741 $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); 3742 3743 3744 templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { 3745 3746 var modalScope = (modalOptions.scope || $rootScope).$new(); 3747 modalScope.$close = modalInstance.close; 3748 modalScope.$dismiss = modalInstance.dismiss; 3749 3750 var ctrlInstance, ctrlLocals = {}; 3751 var resolveIter = 1; 3752 3753 //controllers 3754 if (modalOptions.controller) { 3755 ctrlLocals.$scope = modalScope; 3756 ctrlLocals.$modalInstance = modalInstance; 3757 angular.forEach(modalOptions.resolve, function (value, key) { 3758 ctrlLocals[key] = tplAndVars[resolveIter++]; 3759 }); 3760 3761 ctrlInstance = $controller(modalOptions.controller, ctrlLocals); 3762 } 3763 3764 $modalStack.open(modalInstance, { 3765 scope: modalScope, 3766 deferred: modalResultDeferred, 3767 content: tplAndVars[0], 3768 backdrop: modalOptions.backdrop, 3769 keyboard: modalOptions.keyboard, 3770 windowClass: modalOptions.windowClass 3771 }); 3772 3773 }, function resolveError(reason) { 3774 modalResultDeferred.reject(reason); 3775 }); 3776 3777 templateAndResolvePromise.then(function () { 3778 modalOpenedDeferred.resolve(true); 3779 }, function () { 3780 modalOpenedDeferred.reject(false); 3781 }); 3782 3783 return modalInstance; 3784 }; 3785 3786 return $modal; 3787 }] 3788 }; 3789 3790 return $modalProvider; 3791 }); 3792 3793angular.module('ui.bootstrap.pagination', []) 3794 3795.controller('PaginationController', ['$scope', '$attrs', '$parse', '$interpolate', function ($scope, $attrs, $parse, $interpolate) { 3796 var self = this, 3797 setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; 3798 3799 this.init = function(defaultItemsPerPage) { 3800 if ($attrs.itemsPerPage) { 3801 $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { 3802 self.itemsPerPage = parseInt(value, 10); 3803 $scope.totalPages = self.calculateTotalPages(); 3804 }); 3805 } else { 3806 this.itemsPerPage = defaultItemsPerPage; 3807 } 3808 }; 3809 3810 this.noPrevious = function() { 3811 return this.page === 1; 3812 }; 3813 this.noNext = function() { 3814 return this.page === $scope.totalPages; 3815 }; 3816 3817 this.isActive = function(page) { 3818 return this.page === page; 3819 }; 3820 3821 this.calculateTotalPages = function() { 3822 var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); 3823 return Math.max(totalPages || 0, 1); 3824 }; 3825 3826 this.getAttributeValue = function(attribute, defaultValue, interpolate) { 3827 return angular.isDefined(attribute) ? (interpolate ? $interpolate(attribute)($scope.$parent) : $scope.$parent.$eval(attribute)) : defaultValue; 3828 }; 3829 3830 this.render = function() { 3831 this.page = parseInt($scope.page, 10) || 1; 3832 if (this.page > 0 && this.page <= $scope.totalPages) { 3833 $scope.pages = this.getPages(this.page, $scope.totalPages); 3834 } 3835 }; 3836 3837 $scope.selectPage = function(page) { 3838 if ( ! self.isActive(page) && page > 0 && page <= $scope.totalPages) { 3839 $scope.page = page; 3840 $scope.onSelectPage({ page: page }); 3841 } 3842 }; 3843 3844 $scope.$watch('page', function() { 3845 self.render(); 3846 }); 3847 3848 $scope.$watch('totalItems', function() { 3849 $scope.totalPages = self.calculateTotalPages(); 3850 }); 3851 3852 $scope.$watch('totalPages', function(value) { 3853 setNumPages($scope.$parent, value); // Readonly variable 3854 3855 if ( self.page > value ) { 3856 $scope.selectPage(value); 3857 } else { 3858 self.render(); 3859 } 3860 }); 3861}]) 3862 3863.constant('paginationConfig', { 3864 itemsPerPage: 10, 3865 boundaryLinks: false, 3866 directionLinks: true, 3867 firstText: 'First', 3868 previousText: 'Previous', 3869 nextText: 'Next', 3870 lastText: 'Last', 3871 rotate: true 3872}) 3873 3874.directive('pagination', ['$parse', 'paginationConfig', function($parse, config) { 3875 return { 3876 restrict: 'EA', 3877 scope: { 3878 page: '=', 3879 totalItems: '=', 3880 onSelectPage:' &' 3881 }, 3882 controller: 'PaginationController', 3883 templateUrl: 'template/pagination/pagination.html', 3884 replace: true, 3885 link: function(scope, element, attrs, paginationCtrl) { 3886 3887 // Setup configuration parameters 3888 var maxSize, 3889 boundaryLinks = paginationCtrl.getAttributeValue(attrs.boundaryLinks, config.boun
3889daryLinks ), 3890 directionLinks = paginationCtrl.getAttributeValue(attrs.directionLinks, config.directionLinks ), 3891 firstText = paginationCtrl.getAttributeValue(attrs.firstText, config.firstText, true), 3892 previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true), 3893 nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true), 3894 lastText = paginationCtrl.getAttributeValue(attrs.lastText, config.lastText, true), 3895 rotate = paginationCtrl.getAttributeValue(attrs.rotate, config.rotate); 3896 3897 paginationCtrl.init(config.itemsPerPage); 3898 3899 if (attrs.maxSize) { 3900 scope.$parent.$watch($parse(attrs.maxSize), function(value) { 3901 maxSize = parseInt(value, 10); 3902 paginationCtrl.render(); 3903 }); 3904 } 3905 3906 // Create page object used in template 3907 function makePage(number, text, isActive, isDisabled) { 3908 return { 3909 number: number, 3910 text: text, 3911 active: isActive, 3912 disabled: isDisabled 3913 }; 3914 } 3915 3916 paginationCtrl.getPages = function(currentPage, totalPages) { 3917 var pages = []; 3918 3919 // Default page limits 3920 var startPage = 1, endPage = totalPages; 3921 var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); 3922 3923 // recompute if maxSize 3924 if ( isMaxSized ) { 3925 if ( rotate ) { 3926 // Current page is displayed in the middle of the visible ones 3927 startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); 3928 endPage = startPage + maxSize - 1; 3929 3930 // Adjust if limit is exceeded 3931 if (endPage > totalPages) { 3932 endPage = totalPages; 3933 startPage = endPage - maxSize + 1; 3934 } 3935 } else { 3936 // Visible pages are paginated with maxSize 3937 startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; 3938 3939 // Adjust last page if limit is exceeded 3940 endPage = Math.min(startPage + maxSize - 1, totalPages); 3941 } 3942 } 3943 3944 // Add page number links 3945 for (var number = startPage; number <= endPage; number++) { 3946 var page = makePage(number, number, paginationCtrl.isActive(number), false); 3947 pages.push(page); 3948 } 3949 3950 // Add links to move between page sets 3951 if ( isMaxSized && ! rotate ) { 3952 if ( startPage > 1 ) { 3953 var previousPageSet = makePage(startPage - 1, '...', false, false); 3954 pages.unshift(previousPageSet); 3955 } 3956 3957 if ( endPage < totalPages ) { 3958 var nextPageSet = makePage(endPage + 1, '...', false, false); 3959 pages.push(nextPageSet); 3960 } 3961 } 3962 3963 // Add previous & next links 3964 if (directionLinks) { 3965 var previousPage = makePage(currentPage - 1, previousText, false, paginationCtrl.noPrevious()); 3966 pages.unshift(previousPage); 3967 3968 var nextPage = makePage(currentPage + 1, nextText, false, paginationCtrl.noNext()); 3969 pages.push(nextPage); 3970 } 3971 3972 // Add first & last links 3973 if (boundaryLinks) { 3974 var firstPage = makePage(1, firstText, false, paginationCtrl.noPrevious()); 3975 pages.unshift(firstPage); 3976 3977 var lastPage = makePage(totalPages, lastText, false, paginationCtrl.noNext()); 3978 pages.push(lastPage); 3979 } 3980 3981 return pages; 3982 }; 3983 } 3984 }; 3985}]) 3986 3987.constant('pagerConfig', { 3988 itemsPerPage: 10, 3989 previousText: '« Previous', 3990 nextText: 'Next »', 3991 align: true 3992}) 3993 3994.directive('pager', ['pagerConfig', function(config) { 3995 return { 3996 restrict: 'EA', 3997 scope: { 3998 page: '=', 3999 totalItems: '=', 4000 onSelectPage:' &' 4001 }, 4002 controller: 'PaginationController', 4003 templateUrl: 'template/pagination/pager.html', 4004 replace: true, 4005 link: function(scope, element, attrs, paginationCtrl) { 4006 4007 // Setup configuration parameters 4008 var previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true), 4009 nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true), 4010 align = paginationCtrl.getAttributeValue(attrs.align, config.align); 4011 4012 paginationCtrl.init(config.itemsPerPage); 4013 4014 // Create page object used in template 4015 function makePage(number, text, isDisabled, isPrevious, isNext) { 4016 return { 4017 number: number, 4018 text: text, 4019 disabled: isDisabled, 4020 previous: ( align && isPrevious ), 4021 next: ( align && isNext ) 4022 }; 4023 } 4024 4025 paginationCtrl.getPages = function(currentPage) { 4026 return [ 4027 makePage(currentPage - 1, previousText, paginationCtrl.noPrevious(), true, false), 4028 makePage(currentPage + 1, nextText, paginationCtrl.noNext(), false, true) 4029 ]; 4030 }; 4031 } 4032 }; 4033}]); 4034 4035/** 4036 * The following features are still outstanding: animation as a 4037 * function, placement as a function, inside, support for more triggers than 4038 * just mouse enter/leave, html tooltips, and selector delegation. 4039 */ 4040angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) 4041 4042/** 4043 * The $tooltip service creates tooltip- and popover-like directives as well as 4044 * houses global options for them. 4045 */ 4046.provider( '$tooltip', function () { 4047 // The default options tooltip and popover. 4048 var defaultOptions = { 4049 placement: 'top', 4050 animation: true, 4051 popupDelay: 0 4052 }; 4053 4054 // Default hide triggers for each show trigger 4055 var triggerMap = { 4056 'mouseenter': 'mouseleave', 4057 'click': 'click',
4058 'focus': 'blur' 4059 }; 4060 4061 // The options specified to the provider globally. 4062 var globalOptions = {}; 4063 4064 /** 4065 * `options({})` allows global configuration of all tooltips in the 4066 * application. 4067 * 4068 * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { 4069 * // place tooltips left instead of top by default 4070 * $tooltipProvider.options( { placement: 'left' } ); 4071 * }); 4072 */ 4073 this.options = function( value ) { 4074 angular.extend( globalOptions, value ); 4075 }; 4076 4077 /** 4078 * This allows you to extend the set of trigger mappings available. E.g.: 4079 * 4080 * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); 4081 */ 4082 this.setTriggers = function setTriggers ( triggers ) { 4083 angular.extend( triggerMap, triggers ); 4084 }; 4085 4086 /** 4087 * This is a helper function for translating camel-case to snake-case. 4088 */ 4089 function snake_case(name){ 4090 var regexp = /[A-Z]/g; 4091 var separator = '-'; 4092 return name.replace(regexp, function(letter, pos) { 4093 return (pos ? separator : '') + letter.toLowerCase(); 4094 }); 4095 } 4096 4097 /** 4098 * Returns the actual instance of the $tooltip service. 4099 * TODO support multiple triggers 4100 */ 4101 this.$get = [ '$window', '$compile', '$timeout', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) { 4102 return function $tooltip ( type, prefix, defaultTriggerShow ) { 4103 var options = angular.extend( {}, defaultOptions, globalOptions ); 4104 4105 /** 4106 * Returns an object of show and hide triggers. 4107 * 4108 * If a trigger is supplied, 4109 * it is used to show the tooltip; otherwise, it will use the `trigger` 4110 * option passed to the `$tooltipProvider.options` method; else it will 4111 * default to the trigger supplied to this directive factory. 4112 * 4113 * The hide trigger is based on the show trigger. If the `trigger` option 4114 * was passed to the `$tooltipProvider.options` method, it will use the 4115 * mapped trigger from `triggerMap` or the passed trigger if the map is 4116 * undefined; otherwise, it uses the `triggerMap` value of the show 4117 * trigger; else it will just use the show trigger. 4118 */ 4119 function getTriggers ( trigger ) { 4120 var show = trigger || options.trigger || defaultTriggerShow; 4121 var hide = triggerMap[show] || show; 4122 return { 4123 show: show, 4124 hide: hide 4125 }; 4126 } 4127 4128 var directiveName = snake_case( type ); 4129 4130 var startSym = $interpolate.startSymbol(); 4131 var endSym = $interpolate.endSymbol(); 4132 var template = 4133 '<'+ directiveName +'-popup '+ 4134 'title="'+startSym+'tt_title'+endSym+'" '+ 4135 'content="'+startSym+'tt_content'+endSym+'" '+ 4136 'placement="'+startSym+'tt_placement'+endSym+'" '+ 4137 'animation="tt_animation" '+ 4138 'is-open="tt_isOpen"'+ 4139 '>'+ 4140 '</'+ directiveName +'-popup>'; 4141 4142 return { 4143 restrict: 'EA', 4144 scope: true, 4145 link: function link ( scope, element, attrs ) { 4146 var tooltip = $compile( template )( scope ); 4147 var transitionTimeout; 4148 var popupTimeout; 4149 var $body = $document.find( 'body' ); 4150 var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; 4151 var triggers = getTriggers( undefined ); 4152 var hasRegisteredTriggers = false; 4153 var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); 4154 4155 // By default, the tooltip is not open. 4156 // TODO add ability to start tooltip opened 4157 scope.tt_isOpen = false; 4158 4159 function toggleTooltipBind () { 4160 if ( ! scope.tt_isOpen ) { 4161 showTooltipBind(); 4162 } else { 4163 hideTooltipBind(); 4164 } 4165 } 4166 4167 // Show the tooltip with delay if specified, otherwise show it immediately 4168 function showTooltipBind() { 4169 if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { 4170 return; 4171 } 4172 if ( scope.tt_popupDelay ) { 4173 popupTimeout = $timeout( show, scope.tt_popupDelay ); 4174 } else { 4175 scope.$apply( show ); 4176 } 4177 } 4178 4179 function hideTooltipBind () { 4180 scope.$apply(function () { 4181 hide(); 4182 }); 4183 } 4184 4185 // Show the tooltip popup element. 4186 function show() { 4187 var position, 4188 ttWidth, 4189 ttHeight, 4190 ttPosition; 4191 4192 // Don't show empty tooltips. 4193 if ( ! scope.tt_content ) { 4194 return; 4195 } 4196 4197 // If there is a pending remove transition, we must cancel it, lest the 4198 // tooltip be mysteriously removed. 4199 if ( transitionTimeout ) { 4200 $timeout.cancel( transitionTimeout ); 4201 } 4202 4203 // Set the initial positioning. 4204 tooltip.css({ top: 0, left: 0, display: 'block' }); 4205 4206 // Now we add it to the DOM because need some info about it. But it's not 4207 // visible yet anyway. 4208 if ( appendToBody ) { 4209 $body.append( tooltip ); 4210 } else { 4211 element.after( tooltip ); 4212 } 4213 4214 // Get the position of the directive element. 4215 position = appendToBody ? $position.offset( element ) : $position.position( element ); 4216 4217 // Get the height and width of the tooltip so we can center it. 4218 ttWidth = tooltip.prop( 'offsetWidth' ); 4219 ttHeight = tooltip.prop( 'offsetHeight' ); 4220 4221 // Calculate the tooltip's top and left coordinates to center it with 4222 // this directive. 4223 switch ( scope.tt_placement ) { 4224 case 'right': 4225 ttPosition = { 4226 top: position.top + position.height / 2 - ttHeight / 2, 4227 left: position.left + position.width 4228 }; 4229 break; 4230 case 'bottom': 4231 ttPosition = { 4232 top: position.top + position.height, 4233 left: position.left + position.width / 2 - ttWidth / 2 4234 }; 4235 break; 4236 case 'left': 4237 ttPosition = { 4238 top: position.top + position.height / 2 - ttHeight / 2, 4239 left: position.left - ttWidth 4240 }; 4241 break; 4242 default: 4243 ttPosition = { 4244 top: position.top - ttHeight, 4245 left: position.left + position.width / 2 - ttWidth / 2 4246 }; 4247 break; 4248 } 4249 4250 ttPosition.top += 'px'; 4251 ttPosition.left += 'px'; 4252 4253 // Now set the calculated positioning. 4254 tooltip.css( ttPosition ); 4255 4256 // And show the tooltip. 4257 scope.tt_isOpen = true; 4258 } 4259 4260 // Hide the tooltip popup element. 4261 function hide() { 4262 // First things first: we don't show it anymore. 4263 scope.tt_isOpen = false;
4264 4265 //if tooltip is going to be shown after delay, we must cancel this 4266 $timeout.cancel( popupTimeout ); 4267 4268 // And now we remove it from the DOM. However, if we have animation, we 4269 // need to wait for it to expire beforehand. 4270 // FIXME: this is a placeholder for a port of the transitions library. 4271 if ( scope.tt_animation ) { 4272 transitionTimeout = $timeout(function () { 4273 tooltip.remove(); 4274 }, 500); 4275 } else { 4276 tooltip.remove(); 4277 } 4278 } 4279 4280 /** 4281 * Observe the relevant attributes. 4282 */ 4283 attrs.$observe( type, function ( val ) { 4284 if (val) { 4285 scope.tt_content = val; 4286 } else { 4287 if ( scope.tt_isOpen ) { 4288 hide(); 4289 } 4290 } 4291 }); 4292 4293 attrs.$observe( prefix+'Title', function ( val ) { 4294 scope.tt_title = val; 4295 }); 4296 4297 attrs.$observe( prefix+'Placement', function ( val ) { 4298 scope.tt_placement = angular.isDefined( val ) ? val : options.placement; 4299 }); 4300 4301 attrs.$observe(prefix + 'Animation', function (val) { 4302 scope.tt_animation = angular.isDefined(val) ? !!val : options.animation; 4303 }); 4304 4305 attrs.$observe( prefix+'PopupDelay', function ( val ) { 4306 var delay = parseInt( val, 10 ); 4307 scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; 4308 }); 4309 4310 attrs.$observe( prefix+'Trigger', function ( val ) { 4311 4312 if (hasRegisteredTriggers) { 4313 element.unbind( triggers.show, showTooltipBind ); 4314 element.unbind( triggers.hide, hideTooltipBind ); 4315 } 4316 4317 triggers = getTriggers( val ); 4318 4319 if ( triggers.show === triggers.hide ) { 4320 element.bind( triggers.show, toggleTooltipBind ); 4321 } else { 4322 element.bind( triggers.show, showTooltipBind ); 4323 element.bind( triggers.hide, hideTooltipBind ); 4324 } 4325 4326 hasRegisteredTriggers = true; 4327 }); 4328 4329 attrs.$observe( prefix+'AppendToBody', function ( val ) { 4330 appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; 4331 }); 4332 4333 // if a tooltip is attached to <body> we need to remove it on 4334 // location change as its parent scope will probably not be destroyed 4335 // by the change. 4336 if ( appendToBody ) { 4337 scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { 4338 if ( scope.tt_isOpen ) { 4339 hide(); 4340 } 4341 }); 4342 } 4343 4344 // Make sure tooltip is destroyed and removed. 4345 scope.$on('$destroy', function onDestroyTooltip() { 4346 $timeout.cancel( popupTimeout ); 4347 tooltip.remove(); 4348 tooltip.unbind(); 4349 tooltip = null; 4350 $body = null; 4351 }); 4352 } 4353 }; 4354 }; 4355 }]; 4356}) 4357 4358.directive( 'tooltipPopup', function () { 4359 return { 4360 restrict: 'E', 4361 replace: true, 4362 scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, 4363 templateUrl: 'template/tooltip/tooltip-popup.html' 4364 }; 4365}) 4366 4367.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { 4368 return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); 4369}]) 4370 4371.directive( 'tooltipHtmlUnsafePopup', function () { 4372 return { 4373 restrict: 'E', 4374 replace: true, 4375 scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, 4376 templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' 4377 }; 4378}) 4379 4380.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { 4381 return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); 4382}]); 4383 4384/** 4385 * The following features are still outstanding: popup delay, animation as a 4386 * function, placement as a function, inside, support for more triggers than 4387 * just mouse enter/leave, html popovers, and selector delegatation. 4388 */ 4389angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) 4390.directive( 'popoverPopup', function () { 4391 return { 4392 restrict: 'EA', 4393 replace: true, 4394 scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, 4395 templateUrl: 'template/popover/popover.html' 4396 }; 4397}) 4398.directive( 'popover', [ '$compile', '$timeout', '$parse', '$window', '$tooltip', function ( $compile, $timeout, $parse, $window, $tooltip ) { 4399 return $tooltip( 'popover', 'popover', 'click' ); 4400}]); 4401 4402 4403angular.module('ui.bootstrap.progressbar', ['ui.bootstrap.transition']) 4404 4405.constant('progressConfig', { 4406 animate: true, 4407 autoType: false, 4408 stackedTypes: ['success', 'info', 'warning', 'danger'] 4409}) 4410 4411.controller('ProgressBarController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { 4412 4413 // Whether bar transitions should be animated 4414 var animate = angular.isDefined($attrs.animate) ? $scope.$eval($attrs.animate) : progressConfig.animate; 4415 var autoType = angular.isDefined($attrs.autoType) ? $scope.$eval($attrs.autoType) : progressConfig.autoType; 4416 var stackedTypes = angular.isDefined($attrs.stackedTypes) ? $scope.$eval('[' + $attrs.stackedTypes + ']') : progressConfig.stackedTypes; 4417 4418 // Create bar object 4419 this.makeBar = function(newBar, oldBar, index) { 4420 var newValue = (angular.isObject(newBar)) ? newBar.value : (newB
4420ar || 0); 4421 var oldValue = (angular.isObject(oldBar)) ? oldBar.value : (oldBar || 0); 4422 var type = (angular.isObject(newBar) && angular.isDefined(newBar.type)) ? newBar.type : (autoType) ? getStackedType(index || 0) : null; 4423 4424 return { 4425 from: oldValue, 4426 to: newValue, 4427 type: type, 4428 animate: animate 4429 }; 4430 }; 4431 4432 function getStackedType(index) { 4433 return stackedTypes[index]; 4434 } 4435 4436 this.addBar = function(bar) { 4437 $scope.bars.push(bar); 4438 $scope.totalPercent += bar.to; 4439 }; 4440 4441 this.clearBars = function() { 4442 $scope.bars = []; 4443 $scope.totalPercent = 0; 4444 }; 4445 this.clearBars(); 4446}]) 4447 4448.directive('progress', function() { 4449 return { 4450 restrict: 'EA', 4451 replace: true, 4452 controller: 'ProgressBarController', 4453 scope: { 4454 value: '=percent', 4455 onFull: '&', 4456 onEmpty: '&' 4457 }, 4458 templateUrl: 'template/progressbar/progress.html', 4459 link: function(scope, element, attrs, controller) { 4460 scope.$watch('value', function(newValue, oldValue) { 4461 controller.clearBars(); 4462 4463 if (angular.isArray(newValue)) { 4464 // Stacked progress bar 4465 for (var i=0, n=newValue.length; i < n; i++) { 4466 controller.addBar(controller.makeBar(newValue[i], oldValue[i], i)); 4467 } 4468 } else { 4469 // Simple bar 4470 controller.addBar(controller.makeBar(newValue, oldValue)); 4471 } 4472 }, true); 4473 4474 // Total percent listeners 4475 scope.$watch('totalPercent', function(value) { 4476 if (value >= 100) { 4477 scope.onFull(); 4478 } else if (value <= 0) { 4479 scope.onEmpty(); 4480 } 4481 }, true); 4482 } 4483 }; 4484}) 4485 4486.directive('progressbar', ['$transition', function($transition) { 4487 return { 4488 restrict: 'EA', 4489 replace: true, 4490 scope: { 4491 width: '=', 4492 old: '=', 4493 type: '=', 4494 animate: '=' 4495 }, 4496 templateUrl: 'template/progressbar/bar.html', 4497 link: function(scope, element) { 4498 scope.$watch('width', function(value) { 4499 if (scope.animate) { 4500 element.css('width', scope.old + '%'); 4501 $transition(element, {width: value + '%'}); 4502 } else { 4503 element.css('width', value + '%'); 4504 } 4505 }); 4506 } 4507 }; 4508}]); 4509angular.module('ui.bootstrap.rating', []) 4510 4511.constant('ratingConfig', { 4512 max: 5, 4513 stateOn: null, 4514 stateOff: null 4515}) 4516 4517.controller('RatingController', ['$scope', '$attrs', '$parse', 'ratingConfig', function($scope, $attrs, $parse, ratingConfig) { 4518 4519 this.maxRange = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max; 4520 this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; 4521 this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; 4522 4523 this.createRateObjects = function(states) { 4524 var defaultOptions = { 4525 stateOn: this.stateOn, 4526 stateOff: this.stateOff 4527 }; 4528 4529 for (var i = 0, n = states.length; i < n; i++) { 4530 states[i] = angular.extend({ index: i }, defaultOptions, states[i]); 4531 } 4532 return states; 4533 }; 4534 4535 // Get objects used in template 4536 $scope.range = angular.isDefined($attrs.ratingStates) ? this.createRateObjects(angular.copy($scope.$parent.$eval($attrs.ratingStates))): this.createRateObjects(new Array(this.maxRange)); 4537 4538 $scope.rate = function(value) { 4539 if ( $scope.readonly || $scope.value === value) { 4540 return; 4541 } 4542 4543 $scope.value = value; 4544 }; 4545 4546 $scope.enter = function(value) { 4547 if ( ! $scope.readonly ) { 4548 $scope.val = value; 4549 } 4550 $scope.onHover({value: value}); 4551 }; 4552 4553 $scope.reset = function() { 4554 $scope.val = angular.copy($scope.value); 4555 $scope.onLeave(); 4556 }; 4557 4558 $scope.$watch('value', function(value) { 4559 $scope.val = value; 4560 }); 4561 4562 $scope.readonly = false;
4563 if ($attrs.readonly) { 4564 $scope.$parent.$watch($parse($attrs.readonly), function(value) { 4565 $scope.readonly = !!value; 4566 }); 4567 } 4568}]) 4569 4570.directive('rating', function() { 4571 return { 4572 restrict: 'EA', 4573 scope: { 4574 value: '=', 4575 onHover: '&', 4576 onLeave: '&' 4577 }, 4578 controller: 'RatingController', 4579 templateUrl: 'template/rating/rating.html', 4580 replace: true 4581 }; 4582}); 4583 4584/** 4585 * @ngdoc overview 4586 * @name ui.bootstrap.tabs 4587 * 4588 * @description 4589 * AngularJS version of the tabs directive. 4590 */ 4591 4592angular.module('ui.bootstrap.tabs', []) 4593 4594.directive('tabs', function() { 4595 return function() { 4596 throw new Error("The `tabs` directive is deprecated, please migrate to `tabset`. Instructions can be found at http://github.com/angular-ui/bootstrap/tree/master/CHANGELOG.md"); 4597 }; 4598}) 4599 4600.controller('TabsetController', ['$scope', function TabsetCtrl($scope) { 4601 var ctrl = this, 4602 tabs = ctrl.tabs = $scope.tabs = []; 4603 4604 ctrl.select = function(tab) { 4605 angular.forEach(tabs, function(tab) { 4606 tab.active = false; 4607 }); 4608 tab.active = true; 4609 }; 4610 4611 ctrl.addTab = function addTab(tab) { 4612 tabs.push(tab); 4613 if (tabs.length === 1 || tab.active) { 4614 ctrl.select(tab); 4615 } 4616 }; 4617 4618 ctrl.removeTab = function removeTab(tab) { 4619 var index = tabs.indexOf(tab); 4620 //Select a new tab if the tab to be removed is selected 4621 if (tab.active && tabs.length > 1) { 4622 //If this is the last tab, select the previous tab. else, the next tab. 4623 var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; 4624 ctrl.select(tabs[newActiveIndex]); 4625 } 4626 tabs.splice(index, 1); 4627 }; 4628}]) 4629 4630/** 4631 * @ngdoc directive 4632 * @name ui.bootstrap.tabs.directive:tabset 4633 * @restrict EA 4634 * 4635 * @description 4636 * Tabset is the outer container for the tabs directive 4637 * 4638 * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. 4639 * @param {string=} direction What direction the tabs should be rendered. Available: 4640 * 'right', 'left', 'below'. 4641 * 4642 * @example 4643<example module="ui.bootstrap"> 4644 <file name="index.html"> 4645 <tabset> 4646 <tab heading="Vertical Tab 1"><b>First</b> Content!</tab> 4647 <tab heading="Vertical Tab 2"><i>Second</i> Content!</tab> 4648 </tabset> 4649 <hr /> 4650 <tabset vertical="true"> 4651 <tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab> 4652 <tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab> 4653 </tabset> 4654 </file> 4655</example> 4656 */ 4657.directive('tabset', function() { 4658 return { 4659 restrict: 'EA', 4660 transclude: true, 4661 replace: true, 4662 require: '^tabset', 4663 scope: {}, 4664 controller: 'TabsetController', 4665 templateUrl: 'template/tabs/tabset.html', 4666 compile: function(elm, attrs, transclude) { 4667 return function(scope, element, attrs, tabsetCtrl) { 4668 scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; 4669 scope.type = angular.isDefined(attrs.type) ? scope.$parent.$eval(attrs.type) : 'tabs'; 4670 scope.direction = angular.isDefined(attrs.direction) ? scope.$parent.$eval(attrs.direction) : 'top'; 4671 scope.tabsAbove = (scope.direction != 'below'); 4672 tabsetCtrl.$scope = scope; 4673 tabsetCtrl.$transcludeFn = transclude; 4674 }; 4675 } 4676 }; 4677}) 4678 4679/** 4680 * @ngdoc directive 4681 * @name ui.bootstrap.tabs.directive:tab 4682 * @restrict EA 4683 * 4684 * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. 4685 * @param {string=} select An expression to evaluate when the tab is selected. 4686 * @param {boolean=} active A binding, telling whether or not this tab is selected. 4687 * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. 4688 * 4689 * @description 4690 * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. 4691 * 4692 * @example 4693<example module="ui.bootstrap"> 4694 <file name="index.html"> 4695 <div ng-controller="TabsDemoCtrl"> 4696 <button class="btn btn-small" ng-click="items[0].active = true"> 4697 Select item 1, using active binding 4698 </button> 4699 <button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled"> 4700 Enable/disable item 2, using disabled binding 4701 </button> 4702 <br /> 4703 <tabset> 4704 <tab heading="Tab 1">First Tab</tab> 4705 <tab select="alertMe()"> 4706 <tab-heading><i class="icon-bell"></i> Alert me!</tab-heading> 4707 Second Tab, with alert callback and html heading! 4708 </tab> 4709 <tab ng-repeat="item in items" 4710 heading="{{item.title}}" 4711 disabled="item.disabled" 4712 active="item.active"> 4713 {{item.content}} 4714 </tab> 4715 </tabset> 4716 </div> 4717 </file> 4718 <file name="script.js"> 4719 function TabsDemoCtrl($scope) { 4720 $scope.items = [ 4721 { title:"Dynamic Title 1", content:"Dynamic Item 0" }, 4722 { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } 4723 ]; 4724 4725 $scope.alertMe = function() { 4726 setTimeout(function() { 4727 alert("You've selected the alert tab!"); 4728 }); 4729 }; 4730 }; 4731 </file> 4732</example> 4733 */ 4734 4735/** 4736 * @ngdoc directive 4737 * @name ui.bootstrap.tabs.directive:tabHeading 4738 * @restrict EA 4739 * 4740 * @description 4741 * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. 4742 * 4743 * @example 4744<example module="ui.bootstrap"> 4745 <file name="index.html"> 4746 <tabset> 4747 <tab> 4748 <tab-heading><b>HTML</b> in my titles?!</tab-heading> 4749 And some content, too! 4750 </tab> 4751 <tab> 4752 <tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading> 4753 That's right. 4754 </tab> 4755 </tabset> 4756 </file> 4757</example> 4758 */ 4759.directive('tab', ['$parse', function($parse) { 4760 return { 4761 require: '^tabset', 4762 restrict: 'EA', 4763 replace: true, 4764 templateUrl: 'template/tabs/tab.html', 4765 transclude: true, 4766 scope: { 4767 heading: '@',
4768 onSelect: '&select', //This callback is called in contentHeadingTransclude 4769 //once it inserts the tab's content into the dom 4770 onDeselect: '&deselect' 4771 }, 4772 controller: function() { 4773 //Empty controller so other directives can require being 'under' a tab 4774 }, 4775 compile: function(elm, attrs, transclude) { 4776 return function postLink(scope, elm, attrs, tabsetCtrl) { 4777 var getActive, setActive; 4778 if (attrs.active) { 4779 getActive = $parse(attrs.active); 4780 setActive = getActive.assign; 4781 scope.$parent.$watch(getActive, function updateActive(value, oldVal) { 4782 // Avoid re-initializing scope.active as it is already initialized 4783 // below. (watcher is called async during init with value === 4784 // oldVal) 4785 if (value !== oldVal) { 4786 scope.active = !!value; 4787 } 4788 }); 4789 scope.active = getActive(scope.$parent); 4790 } else { 4791 setActive = getActive = angular.noop; 4792 } 4793 4794 scope.$watch('active', function(active) { 4795 // Note this watcher also initializes and assigns scope.active to the 4796 // attrs.active expression.
4797 setActive(scope.$parent, active); 4798 if (active) { 4799 tabsetCtrl.select(scope); 4800 scope.onSelect(); 4801 } else { 4802 scope.onDeselect(); 4803 } 4804 }); 4805 4806 scope.disabled = false; 4807 if ( attrs.disabled ) { 4808 scope.$parent.$watch($parse(attrs.disabled), function(value) { 4809 scope.disabled = !! value; 4810 }); 4811 } 4812 4813 scope.select = function() { 4814 if ( ! scope.disabled ) { 4815 scope.active = true; 4816 } 4817 }; 4818 4819 tabsetCtrl.addTab(scope); 4820 scope.$on('$destroy', function() { 4821 tabsetCtrl.removeTab(scope); 4822 }); 4823 4824 4825 //We need to transclude later, once the content container is ready. 4826 //when this link happens, we're inside a tab heading. 4827 scope.$transcludeFn = transclude; 4828 }; 4829 } 4830 }; 4831}]) 4832 4833.directive('tabHeadingTransclude', [function() { 4834 return { 4835 restrict: 'A', 4836 require: '^tab', 4837 link: function(scope, elm, attrs, tabCtrl) { 4838 scope.$watch('headingElement', function updateHeadingElement(heading) { 4839 if (heading) { 4840 elm.html(''); 4841 elm.append(heading); 4842 } 4843 }); 4844 } 4845 }; 4846}]) 4847 4848.directive('tabContentTransclude', function() { 4849 return { 4850 restrict: 'A', 4851 require: '^tabset', 4852 link: function(scope, elm, attrs) { 4853 var tab = scope.$eval(attrs.tabContentTransclude); 4854 4855 //Now our tab is ready to be transcluded: both the tab heading area 4856 //and the tab content area are loaded. Transclude 'em both. 4857 tab.$transcludeFn(tab.$parent, function(contents) { 4858 angular.forEach(contents, function(node) { 4859 if (isTabHeading(node)) { 4860 //Let tabHeadingTransclude know. 4861 tab.headingElement = node; 4862 } else { 4863 elm.append(node); 4864 } 4865 }); 4866 }); 4867 } 4868 }; 4869 function isTabHeading(node) { 4870 return node.tagName && ( 4871 node.hasAttribute('tab-heading') || 4872 node.hasAttribute('data-tab-heading') || 4873 node.tagName.toLowerCase() === 'tab-heading' || 4874 node.tagName.toLowerCase() === 'data-tab-heading' 4875 ); 4876 } 4877}) 4878 4879.directive('tabsetTitles', function() { 4880 return { 4881 restrict: 'A', 4882 require: '^tabset', 4883 templateUrl: 'template/tabs/tabset-titles.html', 4884 replace: true, 4885 link: function(scope, elm, attrs, tabsetCtrl) { 4886 if (!scope.$eval(attrs.tabsetTitles)) { 4887 elm.remove(); 4888 } else { 4889 //now that tabs location has been decided, transclude the tab titles in 4890 tabsetCtrl.$transcludeFn(tabsetCtrl.$scope.$parent, function(node) { 4891 elm.append(node); 4892 }); 4893 } 4894 } 4895 }; 4896}); 4897 4898angular.module('ui.bootstrap.timepicker', []) 4899 4900.constant('timepickerConfig', { 4901 hourStep: 1, 4902 minuteStep: 1, 4903 showMeridian: true, 4904 meridians: ['AM', 'PM'], 4905 readonlyInput: false, 4906 mousewheel: true 4907}) 4908 4909.directive('timepicker', ['$parse', '$log', 'timepickerConfig', function ($parse, $log, timepickerConfig) { 4910 return { 4911 restrict: 'EA', 4912 require:'?^ngModel', 4913 replace: true, 4914 scope: {}, 4915 templateUrl: 'template/timepicker/timepicker.html', 4916 link: function(scope, element, attrs, ngModel) { 4917 if ( !ngModel ) { 4918 return; // do nothing if no ng-model 4919 } 4920 4921 var selected = new Date(), meridians = timepickerConfig.meridians; 4922 4923 var hourStep = timepickerConfig.hourStep; 4924 if (attrs.hourStep) { 4925 scope.$parent.$watch($parse(attrs.hourStep), function(value) { 4926 hourStep = parseInt(value, 10); 4927 }); 4928 } 4929 4930 var minuteStep = timepickerConfig.minuteStep; 4931 if (attrs.minuteStep) { 4932 scope.$parent.$watch($parse(attrs.minuteStep), function(value) { 4933 minuteStep = parseInt(value, 10); 4934 }); 4935 } 4936 4937 // 12H / 24H mode 4938 scope.showMeridian = timepickerConfig.showMeridian; 4939 if (attrs.showMeridian) { 4940 scope.$parent.$watch($parse(attrs.showMeridian), function(value) { 4941 scope.showMeridian = !!value; 4942 4943 if ( ngModel.$error.time ) { 4944 // Evaluate from template 4945 var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); 4946 if (angular.isDefined( hours ) && angular.isDefined( minutes )) { 4947 selected.setHours( hours ); 4948 refresh(); 4949 } 4950 } else { 4951 updateTemplate(); 4952 } 4953 }); 4954 } 4955
4956 // Get scope.hours in 24H mode if valid 4957 function getHoursFromTemplate ( ) { 4958 var hours = parseInt( scope.hours, 10 ); 4959 var valid = ( scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); 4960 if ( !valid ) { 4961 return undefined; 4962 } 4963 4964 if ( scope.showMeridian ) { 4965 if ( hours === 12 ) { 4966 hours = 0; 4967 } 4968 if ( scope.meridian === meridians[1] ) { 4969 hours = hours + 12; 4970 } 4971 } 4972 return hours; 4973 } 4974 4975 function getMinutesFromTemplate() { 4976 var minutes = parseInt(scope.minutes, 10); 4977 return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; 4978 } 4979 4980 function pad( value ) { 4981 return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; 4982 } 4983 4984 // Input elements 4985 var inputs = element.find('input'), hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1); 4986 4987 // Respond on mousewheel spin 4988 var mousewheel = (angular.isDefined(attrs.mousewheel)) ? scope.$eval(attrs.mousewheel) : timepickerConfig.mousewheel; 4989 if ( mousewheel ) { 4990 4991 var isScrollingUp = function(e) { 4992 if (e.originalEvent) { 4993 e = e.originalEvent; 4994 } 4995 //pick correct delta variable depending on event 4996 var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; 4997 return (e.detail || delta > 0); 4998 }; 4999 5000 hoursInputEl.bind('mousewheel wheel', function(e) { 5001 scope.$apply( (isScrollingUp(e)) ? scope.incrementHours() : scope.decrementHours() ); 5002 e.preventDefault(); 5003 }); 5004 5005 minutesInputEl.bind('mousewheel wheel', function(e) { 5006 scope.$apply( (isScrollingUp(e)) ? scope.incrementMinutes() : scope.decrementMinutes() ); 5007 e.preventDefault(); 5008 }); 5009 } 5010 5011 scope.readonlyInput = (angular.isDefined(attrs.readonlyInput)) ? scope.$eval(attrs.readonlyInput) : timepickerConfig.readonlyInput; 5012 if ( ! scope.readonlyInput ) { 5013 5014 var invalidate = function(invalidHours, invalidMinutes) { 5015 ngModel.$setViewValue( null ); 5016 ngModel.$setValidity('time', false); 5017 if (angular.isDefined(invalidHours)) { 5018 scope.invalidHours = invalidHours; 5019 } 5020 if (angular.isDefined(invalidMinutes)) { 5021 scope.invalidMinutes = invalidMinutes; 5022 } 5023 }; 5024 5025 scope.updateHours = function() { 5026 var hours = getHoursFromTemplate(); 5027 5028 if ( angular.isDefined(hours) ) { 5029 selected.setHours( hours ); 5030 refresh( 'h' ); 5031 } else { 5032 invalidate(true); 5033 } 5034 }; 5035 5036 hoursInputEl.bind('blur', function(e) { 5037 if ( !scope.validHours && scope.hours < 10) { 5038 scope.$apply( function() { 5039 scope.hours = pad( scope.hours ); 5040 }); 5041 } 5042 }); 5043 5044 scope.updateMinutes = function() { 5045 var minutes = getMinutesFromTemplate(); 5046 5047 if ( angular.isDefined(minutes) ) { 5048 selected.setMinutes( minutes ); 5049 refresh( 'm' ); 5050 } else { 5051 invalidate(undefined, true); 5052 } 5053 }; 5054 5055 minutesInputEl.bind('blur', function(e) { 5056 if ( !scope.invalidMinutes && scope.minutes < 10 ) { 5057 scope.$apply( function() { 5058 scope.minutes = pad( scope.minutes ); 5059 }); 5060 } 5061 }); 5062 } else { 5063 scope.updateHours = angular.noop; 5064 scope.updateMinutes = angular.noop; 5065 } 5066 5067 ngModel.$render = function() { 5068 var date = ngModel.$modelValue ? new Date( ngModel.$modelValue ) : null; 5069 5070 if ( isNaN(date) ) { 5071 ngModel.$setValidity('time', false); 5072 $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); 5073 } else { 5074 if ( date ) { 5075 selected = date; 5076 } 5077 makeValid(); 5078 updateTemplate(); 5079 } 5080 }; 5081 5082 // Call internally when we know that model is valid. 5083 function refresh( keyboardChange ) { 5084 makeValid(); 5085 ngModel.$setViewValue( new Date(selected) ); 5086 updateTemplate( keyboardChange ); 5087 } 5088 5089 function makeValid() { 5090 ngModel.$setValidity('time', true); 5091 scope.invalidHours = false;
5092 scope.invalidMinutes = false; 5093 } 5094 5095 function updateTemplate( keyboardChange ) { 5096 var hours = selected.getHours(), minutes = selected.getMinutes(); 5097 5098 if ( scope.showMeridian ) { 5099 hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system 5100 } 5101 scope.hours = keyboardChange === 'h' ? hours : pad(hours); 5102 scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); 5103 scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; 5104 } 5105 5106 function addMinutes( minutes ) { 5107 var dt = new Date( selected.getTime() + minutes * 60000 ); 5108 selected.setHours( dt.getHours(), dt.getMinutes() ); 5109 refresh(); 5110 } 5111 5112 scope.incrementHours = function() { 5113 addMinutes( hourStep * 60 ); 5114 }; 5115 scope.decrementHours = function() { 5116 addMinutes( - hourStep * 60 ); 5117 }; 5118 scope.incrementMinutes = function() { 5119 addMinutes( minuteStep ); 5120 }; 5121 scope.decrementMinutes = function() { 5122 addMinutes( - minuteStep ); 5123 }; 5124 scope.toggleMeridian = function() { 5125 addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); 5126 }; 5127 } 5128 }; 5129}]); 5130 5131angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) 5132 5133/** 5134 * A helper service that can parse typeahead's syntax (string provided by users) 5135 * Extracted to a separate service for ease of unit testing 5136 */ 5137 .factory('typeaheadParser', ['$parse', function ($parse) { 5138 5139 // 00000111000000000000022200000000000000003333333333333330000000000044000 5140 var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/; 5141 5142 return { 5143 parse:function (input) { 5144 5145 var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source; 5146 if (!match) { 5147 throw new Error( 5148 "Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" + 5149 " but got '" + input + "'."); 5150 } 5151 5152 return { 5153 itemName:match[3], 5154 source:$parse(match[4]), 5155 viewMapper:$parse(match[2] || match[1]), 5156 modelMapper:$parse(match[1]) 5157 }; 5158 } 5159 }; 5160}]) 5161 5162 .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', 5163 function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { 5164 5165 var HOT_KEYS = [9, 13, 27, 38, 40]; 5166 5167 return { 5168 require:'ngModel', 5169 link:function (originalScope, element, attrs, modelCtrl) { 5170 5171 //SUPPORTED ATTRIBUTES (OPTIONS) 5172 5173 //minimal no of characters that needs to be entered before typeahead kicks-in 5174 var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; 5175 5176 //minimal wait time after last character typed before typehead kicks-in 5177 var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; 5178 5179 //should it restrict model values to the ones selected from the popup only? 5180 var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; 5181 5182 //binding to a variable that indicates if matches are being retrieved asynchronously 5183 var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; 5184 5185 //a callback executed when a match is selected 5186 var onSelectCallback = $parse(attrs.typeaheadOnSelect); 5187 5188 var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; 5189 5190 //INTERNAL VARIABLES 5191 5192 //model setter executed upon match selection 5193 var $setModelValue = $parse(attrs.ngModel).assign; 5194 5195 //expressions used by typeahead 5196 var parserResult = typeaheadParser.parse(attrs.typeahead); 5197 5198 var hasFocus; 5199 5200 //pop-up element used to display matches 5201 var popUpEl = angular.element('<div typeahead-popup></div>'); 5202 popUpEl.attr({ 5203 matches: 'matches', 5204 active: 'activeIdx', 5205 select: 'select(activeIdx)', 5206 query: 'query', 5207 position: 'position' 5208 }); 5209 //custom item template 5210 if (angular.isDefined(attrs.typeaheadTemplateUrl)) { 5211 popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); 5212 } 5213 5214 //create a child scope for the typeahead directive so we are not polluting original scope 5215 //with typeahead-specific data (matches, query etc.) 5216 var scope = originalScope.$new(); 5217 originalScope.$on('$destroy', function(){ 5218 scope.$destroy(); 5219 }); 5220 5221 var resetMatches = function() { 5222 scope.matches = []; 5223 scope.activeIdx = -1; 5224 }; 5225 5226 var getMatchesAsync = function(inputValue) { 5227 5228 var locals = {$viewValue: inputValue}; 5229 isLoadingSetter(originalScope, true); 5230 $q.when(parserResult.source(originalScope, locals)).then(function(matches) { 5231 5232 //it might happen that several async queries were in progress if a user were typing fast 5233 //but we are interested only in responses that correspond to the current view value 5234 if (inputValue === modelCtrl.$viewValue && hasFocus) { 5235 if (matches.length > 0) { 5236 5237 scope.activeIdx = 0; 5238 scope.matches.length = 0; 5239 5240 //transform labels 5241 for(var i=0; i<matches.length; i++) { 5242 locals[parserResult.itemName] = matches[i]; 5243 scope.matches.push({ 5244 label: parserResult.viewMapper(scope, locals), 5245 model: matches[i] 5246 }); 5247 } 5248 5249 scope.query = inputValue; 5250 //position pop-up with matches - we need to re-calculate its position each time we are opening a window 5251 //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page 5252 //due to other elements being rendered 5253 scope.position = $position.position(element); 5254 scope.position.top = scope.position.top + element.prop('offsetHeight'); 5255 5256 } else { 5257 resetMatches(); 5258 } 5259 isLoadingSetter(originalScope, false); 5260 } 5261 }, function(){ 5262 resetMatches(); 5263 isLoadingSetter(originalScope, false); 5264 }); 5265 }; 5266 5267 resetMatches(); 5268 5269 //we need to propagate user's query so we can higlight matches 5270 scope.query = undefined; 5271 5272 //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later 5273 var timeoutPromise; 5274 5275 //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM 5276 //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue 5277 modelCtrl.$parsers.unshift(function (inputValue) { 5278 5279 hasFocus = true; 5280 5281 if (inputValue && inputValue.length >= minSearch) { 5282 if (waitTime > 0) { 5283 if (timeoutPromise) { 5284 $timeout.cancel(timeoutPromise);//cancel previous timeout 5285 } 5286 timeoutPromise = $timeout(function () { 5287 getMatchesAsync(inputValue); 5288 }, waitTime); 5289 } else { 5290 getMatchesAsync(inputValue); 5291 } 5292 } else { 5293 isLoadingSetter(originalScope, false); 5294 resetMatches(); 5295 } 5296 5297 if (isEditable) { 5298 return inputValue; 5299 } else { 5300 if (!inputValue) { 5301 // Reset in case user had typed something previously. 5302 modelCtrl.$setValidity('editable', true); 5303 return inputValue; 5304 } else { 5305 modelCtrl.$setValidity('editable', false); 5306 return undefined; 5307 } 5308 } 5309 }); 5310 5311 modelCtrl.$formatters.push(function (modelValue) { 5312 5313 var candidateViewValue, emptyViewValue; 5314 var locals = {}; 5315 5316 if (inputFormatter) { 5317 5318 locals['$model'] = modelValue; 5319 return inputFormatter(originalScope, locals); 5320 5321 } else { 5322 5323 //it might happen that we don't have enough info to properly render input value 5324 //we need to check for this situation and simply return model value if we can't apply custom formatting 5325 locals[parserResult.itemName] = modelValue; 5326 candidateViewValue = parserResult.viewMapper(originalScope, locals); 5327 locals[parserResult.itemName] = undefined; 5328 emptyViewValue = parserResult.viewMapper(originalScope, locals); 5329 5330 return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; 5331 } 5332 }); 5333 5334 scope.select = function (activeIdx) { 5335 //called from within the $digest() cycle 5336 var locals = {}; 5337 var model, item; 5338 5339 locals[parserResult.itemName] = item = scope.matches[activeIdx].model; 5340 model = parserResult.modelMapper(originalScope, locals); 5341 $setModelValue(originalScope, model); 5342 modelCtrl.$setValidity('editable', true); 5343 5344 onSelectCallback(originalScope, { 5345 $item: item, 5346 $model: model, 5347 $label: parserResult.viewMapper(originalScope, locals) 5348 }); 5349 5350 resetMatches(); 5351 5352 //return focus to the input element if a mach was selected via a mouse click event 5353 element[0].focus(); 5354 }; 5355 5356 //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) 5357 element.bind('keydown', function (evt) { 5358 5359 //typeahead is open and an "interesting" key was pressed 5360 if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { 5361 if (evt.which === 13) { 5362 evt.preventDefault(); 5363 } 5364 return; 5365 } 5366 5367 evt.preventDefault(); 5368 5369 if (evt.which === 40) { 5370 scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; 5371 scope.$digest(); 5372 5373 } else if (evt.which === 38) { 5374 scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; 5375 scope.$digest(); 5376 5377 } else if (evt.which === 13 || evt.which === 9) { 5378 scope.$apply(function () { 5379 scope.select(scope.activeIdx); 5380 }); 5381 5382 } else if (evt.which === 27) { 5383 evt.stopPropagation(); 5384 5385 resetMatches(); 5386 scope.$digest(); 5387 } 5388 }); 5389 5390 element.bind('blur', function (evt) { 5391 hasFocus = false;
5392 }); 5393 5394 // Keep reference to click handler to unbind it. 5395 var dismissClickHandler = function (evt) { 5396 if (element[0] !== evt.target) { 5397 resetMatches(); 5398 scope.$digest(); 5399 } 5400 }; 5401 5402 $document.bind('click', dismissClickHandler); 5403 5404 originalScope.$on('$destroy', function(){ 5405 $document.unbind('click', dismissClickHandler); 5406 }); 5407 5408 element.after($compile(popUpEl)(scope)); 5409 } 5410 }; 5411 5412}]) 5413 5414 .directive('typeaheadPopup', function () { 5415 return { 5416 restrict:'EA', 5417 scope:{ 5418 matches:'=', 5419 query:'=', 5420 active:'=', 5421 position:'=', 5422 select:'&' 5423 }, 5424 replace:true, 5425 templateUrl:'template/typeahead/typeahead-popup.html', 5426 link:function (scope, element, attrs) { 5427 5428 scope.templateUrl = attrs.templateUrl; 5429 5430 scope.isOpen = function () { 5431 return scope.matches.length > 0; 5432 }; 5433 5434 scope.isActive = function (matchIdx) { 5435 return scope.active == matchIdx; 5436 }; 5437 5438 scope.selectActive = function (matchIdx) { 5439 scope.active = matchIdx; 5440 }; 5441 5442 scope.selectMatch = function (activeIdx) { 5443 scope.select({activeIdx:activeIdx}); 5444 }; 5445 } 5446 }; 5447 }) 5448 5449 .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { 5450 return { 5451 restrict:'EA', 5452 scope:{ 5453 index:'=', 5454 match:'=', 5455 query:'=' 5456 }, 5457 link:function (scope, element, attrs) { 5458 var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; 5459 $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ 5460 element.replaceWith($compile(tplContent.trim())(scope)); 5461 }); 5462 } 5463 }; 5464 }]) 5465 5466 .filter('typeaheadHighlight', function() { 5467 5468 function escapeRegexp(queryToEscape) { 5469 return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); 5470 } 5471 5472 return function(matchItem, query) { 5473 return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem; 5474 }; 5475 }); 5476angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { 5477 $templateCache.put("template/accordion/accordion-group.html", 5478 "<div class=\"accordion-group\">\n" + 5479 " <div class=\"accordion-heading\" ><a class=\"accordion-toggle\" ng-click=\"isOpen = !isOpen\" accordion-transclude=\"heading\">{{heading}}</a></div>\n" + 5480 " <div class=\"accordion-body\" collapse=\"!isOpen\">\n" + 5481 " <div class=\"accordion-inner\" ng-transclude></div> </div>\n" + 5482 "</div>"); 5483}]); 5484 5485angular.module("template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { 5486 $templateCache.put("template/accordion/accordion.html", 5487 "<div class=\"accordion\" ng-transclude></div>"); 5488}]); 5489 5490angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { 5491 $templateCache.put("template/alert/alert.html", 5492 "<div class='alert' ng-class='type && \"alert-\" + type'>\n" + 5493 " <button ng-show='closeable' type='button' class='close' ng-click='close()'>×</button>\n" + 5494 " <div ng-transclude></div>\n" + 5495 "</div>\n" + 5496 ""); 5497}]); 5498 5499angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { 5500 $templateCache.put("template/carousel/carousel.html", 5501 "<div ng-mouseenter=\"pause()\" ng-mouseleave=\"play()\" class=\"carousel\">\n" + 5502 " <ol class=\"carousel-indicators\" ng-show=\"slides().length > 1\">\n" + 5503 " <li ng-repeat=\"slide in slides()\" ng-class=\"{active: isActive(slide)}\" ng-click=\"select(slide)\"></li>\n" + 5504 " </ol>\n" + 5505 " <div class=\"carousel-inner\" ng-transclude></div>\n" + 5506 " <a ng-click=\"prev()\" class=\"carousel-control left\" ng-show=\"slides().length > 1\">‹</a>\n" + 5507 " <a ng-click=\"next()\" class=\"carousel-control right\" ng-show=\"slides().length > 1\">›</a>\n" + 5508 "</div>\n" + 5509 ""); 5510}]); 5511 5512angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { 5513 $templateCache.put("template/carousel/slide.html", 5514 "<div ng-class=\"{\n" + 5515 " 'active': leaving || (active && !entering),\n" + 5516 " 'prev': (next || active) && direction=='prev',\n" + 5517 " 'next': (next || active) && direction=='next',\n" + 5518 " 'right': direction=='prev',\n" + 5519 " 'left': direction=='next'\n" + 5520 " }\" class=\"item\" ng-transclude></div>\n" + 5521 ""); 5522}]); 5523 5524angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { 5525 $templateCache.put("template/datepicker/datepicker.html", 5526 "<table>\n" + 5527 " <thead>\n" + 5528 " <tr class=\"text-center\">\n" + 5529 " <th><button type=\"button\" class=\"btn pull-left\" ng-click=\"move(-1)\"><i class=\"icon-chevron-left\"></i></button></th>\n" + 5530 " <th colspan=\"{{rows[0].length - 2 + showWeekNumbers}}\"><button type=\"button\" class=\"btn btn-block\" ng-click=\"toggleMode()\"><strong>{{title}}</strong></button></th>\n" + 5531 " <th><button type=\"button\" class=\"btn pull-right\" ng-click=\"move(1)\"><i class=\"icon-chevron-right\"></i></button></th>\n" + 5532 " </tr>\n" + 5533 " <tr class=\"text-center\" ng-show=\"labels.length > 0\">\n" + 5534 " <th ng-show=\"showWeekNumbers\">#</th>\n" + 5535 " <th ng-repeat=\"label in labels\">{{label}}</th>\n" + 5536 " </tr>\n" + 5537 " </thead>\n" + 5538 " <tbody>\n" + 5539 " <tr ng-repeat=\"row in rows\">\n" + 5540 " <td ng-show=\"showWeekNumbers\" class=\"text-center\"><em>{{ getWeekNumber(row) }}</em></td>\n" + 5541 " <td ng-repeat=\"dt in row\" class=\"text-center\">\n" + 5542 " <button type=\"button\" style=\"width:100%;\" class=\"btn\" ng-class=\"{'btn-info': dt.selected}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\"><span ng-class=\"{muted: dt.secondary}\">{{dt.label}}</span></button>\n" + 5543 " </td>\n" + 5544 " </tr>\n" + 5545 " </tbody>\n" + 5546 "</table>\n" + 5547 ""); 5548}]); 5549 5550angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { 5551 $templateCache.put("template/datepicker/popup.html", 5552 "<ul class=\"dropdown-menu\" ng-style=\"{display: (isOpen && 'block') || 'none', top: position.top+'px', left: position.left+'px'}\">\n" + 5553 " <li ng-transclude></li>\n" + 5554 " <li class=\"divider\"></li>\n" + 5555 " <li style=\"padding: 9px;\">\n" + 5556 " <span class=\"btn-group\">\n" + 5557 " <button type=\"button\" class=\"btn btn-small btn-inverse\" ng-click=\"today()\">{{currentText}}</button>\n" + 5558 " <button type=\"button\" class=\"btn btn-small btn-info\" ng-click=\"showWeeks = ! showWeeks\" ng-class=\"{active: showWeeks}\">{{toggleWeeksText}}</button>\n" + 5559 " <button type=\"button\" class=\"btn btn-small btn-danger\" ng-click=\"clear()\">{{clearText}}</button>\n" + 5560 " </span>\n" + 5561 " <button type=\"button\" class=\"btn btn-small btn-success pull-right\" ng-click=\"isOpen = false\">{{closeText}}</button>\n" + 5562 " </li>\n" + 5563 "</ul>\n" + 5564 ""); 5565}]); 5566 5567angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { 5568 $templateCache.put("template/modal/backdrop.html", 5569 "<div class=\"modal-backdrop fade\" ng-class=\"{in: animate}
5569\" ng-style=\"{'z-index': 1040 + index*10}\" ng-click=\"close($event)\"></div>"); 5570}]); 5571 5572angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { 5573 $templateCache.put("template/modal/window.html", 5574 "<div class=\"modal fade {{ windowClass }}\" ng-class=\"{in: animate}\" ng-style=\"{'z-index': 1050 + index*10}\" ng-transclude></div>"); 5575}]); 5576 5577angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { 5578 $templateCache.put("template/pagination/pager.html", 5579 "<div class=\"pager\">\n" + 5580 " <ul>\n" + 5581 " <li ng-repeat=\"page in pages\" ng-class=\"{disabled: page.disabled, previous: page.previous, next: page.next}\"><a ng-click=\"selectPage(page.number)\">{{page.text}}</a></li>\n" + 5582 " </ul>\n" + 5583 "</div>\n" + 5584 ""); 5585}]); 5586 5587angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { 5588 $templateCache.put("template/pagination/pagination.html", 5589 "<div class=\"pagination\"><ul>\n" + 5590 " <li ng-repeat=\"page in pages\" ng-class=\"{active: page.active, disabled: page.disabled}\"><a ng-click=\"selectPage(page.number)\">{{page.text}}</a></li>\n" + 5591 " </ul>\n" + 5592 "</div>\n" + 5593 ""); 5594}]); 5595 5596angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) { 5597 $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html", 5598 "<div class=\"tooltip {{placement}}\" ng-class=\"{ in: isOpen(), fade: animation() }\">\n" + 5599 " <div class=\"tooltip-arrow\"></div>\n" + 5600 " <div class=\"tooltip-inner\" bind-html-unsafe=\"content\"></div>\n" + 5601 "</div>\n" + 5602 ""); 5603}]); 5604 5605angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { 5606 $templateCache.put("template/tooltip/tooltip-popup.html", 5607 "<div class=\"tooltip {{placement}}\" ng-class=\"{ in: isOpen(), fade: animation() }\">\n" + 5608 " <div class=\"tooltip-arrow\"></div>\n" + 5609 " <div class=\"tooltip-inner\" ng-bind=\"content\"></div>\n" + 5610 "</div>\n" + 5611 ""); 5612}]); 5613 5614angular.module("template/popover/popover.html", []).run(["$templateCache", function($templateCache) { 5615 $templateCache.put("template/popover/popover.html", 5616 "<div class=\"popover {{placement}}\" ng-class=\"{ in: isOpen(), fade: animation() }\">\n" + 5617 " <div class=\"arrow\"></div>\n" + 5618 "\n" + 5619 " <div class=\"popover-inner\">\n" + 5620 " <h3 class=\"popover-title\" ng-bind=\"title\" ng-show=\"title\"></h3>\n" + 5621 " <div class=\"popover-content\" ng-bind=\"content\"></div>\n" + 5622 " </div>\n" + 5623 "</div>\n" + 5624 ""); 5625}]); 5626 5627angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { 5628 $templateCache.put("template/progressbar/bar.html", 5629 "<div class=\"bar\" ng-class='type && \"bar-\" + type'></div>"); 5630}]); 5631 5632angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { 5633 $templateCache.put("template/progressbar/progress.html", 5634 "<div class=\"progress\"><progressbar ng-repeat=\"bar in bars\" width=\"bar.to\" old=\"bar.from\" animate=\"bar.animate\" type=\"bar.type\"></progressbar></div>"); 5635}]); 5636 5637angular.module("template/rating/rating.html", []).run(["$templateCache", function($templateCache) { 5638 $templateCache.put("template/rating/rating.html", 5639 "<span ng-mouseleave=\"reset()\">\n" + 5640 " <i ng-repeat=\"r in range\" ng-mouseenter=\"enter($index + 1)\" ng-click=\"rate($index + 1)\" ng-class=\"$index < val && (r.stateOn || 'icon-star') || (r.stateOff || 'icon-star-empty')\"></i>\n" + 5641 "</span>"); 5642}]); 5643 5644angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { 5645 $templateCache.put("template/tabs/tab.html", 5646 "<li ng-class=\"{active: active, disabled: disabled}\">\n" + 5647 " <a ng-click=\"select()\" tab-heading-transclude>{{heading}}</a>\n" + 5648 "</li>\n" + 5649 ""); 5650}]); 5651 5652angular.module("template/tabs/tabset-titles.html", []).run(["$templateCache", function($templateCache) { 5653 $templateCache.put("template/tabs/tabset-titles.html", 5654 "<ul class=\"nav {{type && 'nav-' + type}}\" ng-class=\"{'nav-stacked': vertical}\">\n" + 5655 "</ul>\n" + 5656 ""); 5657}]); 5658 5659angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { 5660 $templateCache.put("template/tabs/tabset.html", 5661 "\n" + 5662 "<div class=\"tabbable\" ng-class=\"{'tabs-right': direction == 'right', 'tabs-left': direction == 'left', 'tabs-below': direction == 'below'}\">\n" + 5663 " <div tabset-titles=\"tabsAbove\"></div>\n" + 5664 " <div class=\"tab-content\">\n" + 5665 " <div class=\"tab-pane\" \n" + 5666 " ng-repeat=\"tab in tabs\" \n" + 5667 " ng-class=\"{active: tab.active}\"\n" + 5668 " tab-content-transclude=\"tab\">\n" + 5669 " </div>\n" + 5670 " </div>\n" + 5671 " <div tabset-titles=\"!tabsAbove\"></div>\n" + 5672 "</div>\n" + 5673 ""); 5674}]); 5675 5676angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { 5677 $templateCache.put("template/timepicker/timepicker.html", 5678 "<table class=\"form-inline\">\n" + 5679 " <tr class=\"text-center\">\n" + 5680 " <td><a ng-click=\"incrementHours()\" class=\"btn btn-link\"><i class=\"icon-chevron-up\"></i></a></td>\n" + 5681 " <td> </td>\n" + 5682 " <td><a ng-click=\"incrementMinutes()\" class=\"btn btn-link\"><i class=\"icon-chevron-up\"></i></a></td>\n" + 5683 " <td ng-show=\"showMeridian\"></td>\n" + 5684 " </tr>\n" + 5685 " <tr>\n" + 5686 " <td class=\"control-group\" ng-class=\"{'error': invalidHours}\"><input type=\"text\" ng-model=\"hours\" ng-change=\"updateHours()\" class=\"span1 text-center\" ng-mousewheel=\"incrementHours()\" ng-readonly=\"readonlyInput\" maxlength=\"2\"></td>\n" + 5687 " <td>:</td>\n" + 5688 " <td class=\"control-group\" ng-class=\"{'error': invalidMinutes}\"><input type=\"text\" ng-model=\"minutes\" ng-change=\"updateMinutes()\" class=\"span1 text-center\" ng-readonly=\"readonlyInput\" maxlength=\"2\"></td>\n" + 5689 " <td ng-show=\"showMeridian\"><button type=\"button\" ng-click=\"toggleMeridian()\" class=\"btn text-center\">{{meridian}}</button></td>\n" + 5690 " </tr>\n" + 5691 " <tr class=\"text-center\">\n" + 5692 " <td><a ng-click=\"decrementHours()\" class=\"btn btn-link\"><i class=\"icon-chevron-down\"></i></a></td>\n" + 5693 " <td> </td>\n" + 5694 " <td><a ng-click=\"decrementMinutes()\" class=\"btn btn-link\"><i class=\"icon-chevron-down\"></i></a></td>\n" + 5695 " <td ng-show=\"showMeridian\"></td>\n" + 5696 " </tr>\n" + 5697 "</table>\n" + 5698 ""); 5699}]); 5700 5701angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { 5702 $templateCache.put("template/typeahead/typeahead-match.html", 5703 "<a tabindex=\"-1\" bind-html-unsafe=\"match.label | typeaheadHighlight:query\"></a>"); 5704}]); 5705 5706angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { 5707 $templateCache.put("template/typeahead/typeahead-popup.html", 5708 "<ul class=\"typeahead dropdown-menu\" ng-style=\"{display: isOpen()&&'block' || 'none', top: position.top+'px', left: position.left+'px'}\">\n" + 5709 " <li ng-repeat=\"match in matches\" ng-class=\"{active: isActive($index) }\" ng-mouseenter=\"selectActive($index)\" ng-click=\"selectMatch($index)\">\n" + 5710 " <div typeahead-match index=\"$index\" match=\"match\" query=\"query\" template-url=\"templateUrl\"></div>\n" + 5711 " </li>\n" + 5712 "</ul>"); 5713}]); 5714 5715/** 5716 * @license AngularJS v1.2.6 5717 * (c) 2010-2014 Google, Inc. http://angularjs.org 5718 * License: MIT 5719 */ 5720(function(window, angular, undefined) {'use strict'; 5721 5722/* jshint maxlen: false */ 5723 5724/** 5725 * @ngdoc overview 5726 * @name ngAnimate 5727 * @description 5728 * 5729 * # ngAnimate 5730 * 5731 * The `ngAnimate` module provides support for JavaScript, CSS3 transition and CSS3 keyframe animation hooks within existing core and custom directives. 5732 * 5733 * {@installModule animate} 5734 * 5735 * <div doc-module-components="ngAnimate"></div> 5736 * 5737 * # Usage 5738 * 5739 * To see animations in action, all that is required is to define the appropriate CSS classes 5740 * or to register a JavaScript animation via the myModule.animation() function. The directives that support animation automatically are: 5741 * `ngRepeat`, `ngInclude`, `ngIf`, `ngSwitch`, `ngShow`, `ngHide`, `ngView` and `ngClass`. Custom directives can take advantage of animation 5742 * by using the `$animate` service. 5743 * 5744 * Below is a more detailed breakdown of the supported animation events provided by pre-existing ng directives: 5745 * 5746 * | Directive | Supported Animations | 5747 * |---------------------------------------------------------- |----------------------------------------------------| 5748 * | {@link ng.directive:ngRepeat#usage_animations ngRepeat} | enter, leave and move | 5749 * | {@link ngRoute.directive:ngView#usage_animations ngView} | enter and leave | 5750 * | {@link ng.directive:ngInclude#usage_animations ngInclude} | enter and leave | 5751 * | {@link ng.directive:ngSwitch#usage_animations ngSwitch} | enter and leave | 5752 * | {@link ng.directive:ngIf#usage_animations ngIf} | enter and leave | 5753 * | {@link ng.directive:ngClass#usage_animations ngClass} | add and remove | 5754 * | {@link ng.directive:ngShow#usage_animations ngShow & ngHide} | add and remove (the ng-hide class value) | 5755 * 5756 * You can find out more information about animations upon visiting each directive page. 5757 * 5758 * Below is an example of how to apply animations to a directive that supports animation hooks: 5759 * 5760 * <pre> 5761 * <style type="text/css"> 5762 * .slide.ng-enter, .slide.ng-leave { 5763 * -webkit-transition:0.5s linear all; 5764 * transition:0.5s linear all; 5765 * } 5766 * 5767 * .slide.ng-enter { } /* starting animations for enter */ 5768 * .slide.ng-enter-active { } /* terminal animations for enter */ 5769 * .slide.ng-leave { } /* starting animations for leave */ 5770 * .slide.ng-leave-active { } /* terminal animations for leave */ 5771 * </style> 5772 * 5773 * <!-- 5774 * the animate service will automatically add .ng-enter and .ng-leave to the element 5775 * to trigger the CSS transition/animations 5776 * --> 5777 * <ANY class="slide" ng-include="..."></ANY> 5778 * </pre> 5779 * 5780 * Keep in mind that if an animation is running, any child elements cannot be animated until the parent element's 5781 * animation has completed. 5782 * 5783 * <h2>CSS-defined Animations</h2> 5784 * The animate service will automatically apply two CSS classes to the animated element and these two CSS classes 5785 * are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported 5786 * and can be used to play along with this naming structure. 5787 * 5788 * The following code below demonstrates how to perform animations using **CSS transitions** with Angular: 5789 * 5790 * <pre> 5791 * <style type="text/css"> 5792 * /* 5793 * The animate class is apart of the element and the ng-enter class 5794 * is attached to the element once the enter animation event is triggered 5795 * */ 5796 * .reveal-animation.ng-enter { 5797 * -webkit-transition: 1s linear all; /* Safari/Chrome */ 5798 * transition: 1s linear all; /* All other modern browsers and IE10+ */ 5799 * 5800 * /* The animation preparation code */ 5801 * opacity: 0; 5802 * } 5803 * 5804 * /* 5805 * Keep in mind that you want to combine both CSS 5806 * classes together to avoid any CSS-specificity 5807 * conflicts 5808 * */ 5809 * .reveal-animation.ng-enter.ng-enter-active { 5810 * /* The animation code itself */ 5811 * opacity: 1; 5812 * } 5813 * </style> 5814 * 5815 * <div class="view-container"> 5816 * <div ng-view class="reveal-animation"></div> 5817 * </div> 5818 * </pre> 5819 * 5820 * The following code below demonstrates how to perform animations using **CSS animations** with Angular: 5821 * 5822 * <pre> 5823 * <style type="text/css"> 5824 * .reveal-animation.ng-enter { 5825 * -webkit-animation: enter_sequence 1s linear; /* Safari/Chrome */ 5826 * animation: enter_sequence 1s linear; /* IE10+ and Future Browsers */ 5827 * } 5828 * @-webkit-keyframes enter_sequence { 5829 * from { opacity:0; } 5830 * to { opacity:1; } 5831 * } 5832 * @keyframes enter_sequence { 5833 * from { opacity:0; } 5834 * to { opacity:1; } 5835 * } 5836 * </style> 5837 * 5838 * <div class="view-container"> 5839 * <div ng-view class="reveal-animation"></div> 5840 * </div> 5841 * </pre> 5842 * 5843 * Both CSS3 animations and transitions can be used together and the animate service will figure out the correct duration and delay timing. 5844 * 5845 * Upon DOM mutation, the event class is added first (something like `ng-enter`), then the browser prepares itself to add 5846 * the active class (in this case `ng-enter-active`) which then triggers the animation. The animation module will automatically 5847 * detect the CSS code to determine when the animation ends. Once the animation is over then both CSS classes will be 5848 * removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end 5849 * immediately resulting in a DOM element that is at its final state. This final state is when the DOM element 5850 * has no CSS transition/animation classes applied to it. 5851 * 5852 * <h3>CSS Staggering Animations</h3> 5853 * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a 5854 * curtain-like effect. The ngAnimate module, as of 1.2.0, supports staggering animations and the stagger effect can be 5855 * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for 5856 * the animation. The style property expected within the stagger class can either be a **transition-delay** or an 5857 * **animation-delay** property (or both if your animation contains both transitions and keyframe animations). 5858 * 5859 * <pre> 5860 * .my-animation.ng-enter { 5861 * /* standard transition code */ 5862 * -webkit-transition: 1s linear all; 5863 * transition: 1s linear all; 5864 * opacity:0; 5865 * } 5866 * .my-animation.ng-enter-stagger { 5867 * /* this will have a 100ms delay between each successive leave animation */ 5868 * -webkit-transition-delay: 0.1s; 5869 * transition-delay: 0.1s; 5870 * 5871 * /* in case the stagger doesn't work then these two values 5872 * must be set to 0 to avoid an accidental CSS inheritance */ 5873 * -webkit-transition-duration: 0s; 5874 * transition-duration: 0s; 5875 * } 5876 * .my-animation.ng-enter.ng-enter-active { 5877 * /* standard transition styles */ 5878 * opacity:1; 5879 * } 5880 * </pre> 5881 * 5882 * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations 5883 * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this 5884 * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation 5885 * will also be reset if more than 10ms has passed after the last animation has been fired. 5886 * 5887 * The following code will issue the **ng-leave-stagger** event on the element provided: 5888 * 5889 * <pre> 5890 * var kids = parent.children(); 5891 * 5892 * $animate.leave(kids[0]); //stagger index=0 5893 * $animate.leave(kids[1]); //stagger index=1 5894 * $animate.leave(kids[2]); //stagger index=2 5895 * $animate.leave(kids[3]); //stagger index=3 5896 * $animate.leave(kids[4]); //stagger index=4 5897 * 5898 * $timeout(function() { 5899 * //stagger has reset itself 5900 * $animate.leave(kids[5]); //stagger index=0 5901 * $animate.leave(kids[6]); //stagger index=1 5902 * }, 100, false); 5903 * </pre> 5904 * 5905 * Stagger animations are currently only supported within CSS-defined animations. 5906 * 5907 * <h2>JavaScript-defined Animations</h2> 5908 * In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not 5909 * yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module. 5910 * 5911 * <pre> 5912 * //!annotate="YourApp" Your AngularJS Module|Replace this or ngModule with the module that you used to define your a
5912pplication. 5913 * var ngModule = angular.module('YourApp', ['ngAnimate']); 5914 * ngModule.animation('.my-crazy-animation', function() { 5915 * return { 5916 * enter: function(element, done) { 5917 * //run the animation here and call done when the animation is complete 5918 * return function(cancelled) { 5919 * //this (optional) function will be called when the animation 5920 * //completes or when the animation is cancelled (the cancelled 5921 * //flag will be set to true if cancelled). 5922 * }; 5923 * }, 5924 * leave: function(element, done) { }, 5925 * move: function(element, done) { }, 5926 * 5927 * //animation that can be triggered before the class is added 5928 * beforeAddClass: function(element, className, done) { }, 5929 * 5930 * //animation that can be triggered after the class is added 5931 * addClass: function(element, className, done) { }, 5932 * 5933 * //animation that can be triggered before the class is removed 5934 * beforeRemoveClass: function(element, className, done) { }, 5935 * 5936 * //animation that can be triggered after the class is removed 5937 * removeClass: function(element, className, done) { } 5938 * }; 5939 * }); 5940 * </pre> 5941 * 5942 * JavaScript-defined animations are created with a CSS-like class selector and a collection of events which are set to run 5943 * a javascript callback function. When an animation is triggered, $animate will look for a matching animation which fits 5944 * the element's CSS class attribute value and then run the matching animation event function (if found). 5945 * In other words, if the CSS classes present on the animated element match any of the JavaScript animations then the callback function will 5946 * be executed. It should be also noted that only simple, single class selectors are allowed (compound class selectors are not supported). 5947 * 5948 * Within a JavaScript animation, an object containing various event callback animation functions is expected to be returned. 5949 * As explained above, these callbacks are triggered based on the animation event. Therefore if an enter animation is run, 5950 * and the JavaScript animation is found, then the enter callback will handle that animation (in addition to the CSS keyframe animation 5951 * or transition code that is defined via a stylesheet). 5952 * 5953 */ 5954 5955angular.module('ngAnimate', ['ng']) 5956 5957 /** 5958 * @ngdoc object 5959 * @name ngAnimate.$animateProvider 5960 * @description 5961 * 5962 * The `$animateProvider` allows developers to register JavaScript animation event handlers directly inside of a module. 5963 * When an animation is triggered, the $animate service will query the $animate service to find any animations that match 5964 * the provided name value. 5965 * 5966 * Requires the {@link ngAnimate `ngAnimate`} module to be installed. 5967 * 5968 * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application. 5969 * 5970 */ 5971 .config(['$provide', '$animateProvider', function($provide, $animateProvider) { 5972 var noop = angular.noop; 5973 var forEach = angular.forEach; 5974 var selectors = $animateProvider.$$selectors; 5975 5976 var ELEMENT_NODE = 1; 5977 var NG_ANIMATE_STATE = '$$ngAnimateState'; 5978 var NG_ANIMATE_CLASS_NAME = 'ng-animate'; 5979 var rootAnimateState = {running: true}; 5980 5981 function extractElementNode(element) { 5982 for(var i = 0; i < element.length; i++) { 5983 var elm = element[i]; 5984 if(elm.nodeType == ELEMENT_NODE) { 5985 return elm; 5986 } 5987 } 5988 } 5989 5990 function isMatchingElement(elm1, elm2) { 5991 return extractElementNode(elm1) == extractElementNode(elm2); 5992 } 5993 5994 $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope', '$document', 5995 function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope, $document) { 5996 5997 $rootElement.data(NG_ANIMATE_STATE, rootAnimateState); 5998 5999 // disable animations during bootstrap, but once we bootstrapped, wait again 6000 // for another digest until enabling animations. The reason why we digest twice 6001 // is because all structural animations (enter, leave and move) all perform a 6002 // post digest operation before animating. If we only wait for a single digest 6003 // to pass then the structural animation would render its animation on page load. 6004 // (which is what we're trying to avoid when the application first boots up.) 6005 $rootScope.$$postDigest(function() { 6006 $rootScope.$$postDigest(function() { 6007 rootAnimateState.running = false;
6008 }); 6009 }); 6010 6011 var classNameFilter = $animateProvider.classNameFilter(); 6012 var isAnimatableClassName = !classNameFilter 6013 ? function() { return true; } 6014 : function(className) { 6015 return classNameFilter.test(className); 6016 }; 6017 6018 function lookup(name) { 6019 if (name) { 6020 var matches = [], 6021 flagMap = {}, 6022 classes = name.substr(1).split('.'); 6023 6024 //the empty string value is the default animation 6025 //operation which performs CSS transition and keyframe 6026 //animations sniffing. This is always included for each 6027 //element animation procedure if the browser supports 6028 //transitions and/or keyframe animations 6029 if ($sniffer.transitions || $sniffer.animations) { 6030 classes.push(''); 6031 } 6032 6033 for(var i=0; i < classes.length; i++) { 6034 var klass = classes[i], 6035 selectorFactoryName = selectors[klass]; 6036 if(selectorFactoryName && !flagMap[klass]) { 6037 matches.push($injector.get(selectorFactoryName)); 6038 flagMap[klass] = true; 6039 } 6040 } 6041 return matches; 6042 } 6043 } 6044 6045 /** 6046 * @ngdoc object 6047 * @name ngAnimate.$animate 6048 * @function 6049 * 6050 * @description 6051 * The `$animate` service provides animation detection support while performing DOM operations (enter, leave and move) as well as during addClass and removeClass operations. 6052 * When any of these operations are run, the $animate service 6053 * will examine any JavaScript-defined animations (which are defined by using the $animateProvider provider object) 6054 * as well as any CSS-defined animations against the CSS classes present on the element once the DOM operation is run. 6055 * 6056 * The `$animate` service is used behind the scenes with pre-existing directives and animation with these directives 6057 * will work out of the box without any extra configuration. 6058 * 6059 * Requires the {@link ngAnimate `ngAnimate`} module to be installed. 6060 * 6061 * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application. 6062 * 6063 */ 6064 return { 6065 /** 6066 * @ngdoc function 6067 * @name ngAnimate.$animate#enter 6068 * @methodOf ngAnimate.$animate 6069 * @function 6070 * 6071 * @description 6072 * Appends the element to the parentElement element that resides in the document and then runs the enter animation. Once 6073 * the animation is started, the following CSS classes will be present on the element for the duration of the animation: 6074 * 6075 * Below is a breakdown of each step that occurs during enter animation: 6076 * 6077 * | Animation Step | What the element class attribute looks like | 6078 * |----------------------------------------------------------------------------------------------|---------------------------------------------| 6079 * | 1. $animate.enter(...) is called | class="my-animation" | 6080 * | 2. element is inserted into the parentElement element or beside the afterElement element | class="my-animation" | 6081 * | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | 6082 * | 4. the .ng-enter class is added to the element | class="my-animation ng-animate ng-enter" | 6083 * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-enter" | 6084 * | 6. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate ng-enter" | 6085 * | 7. the .ng-enter-active and .ng-animate-active classes are added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active ng-enter ng-enter-active" | 6086 * | 8. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active ng-enter ng-enter-active" | 6087 * | 9. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | 6088 * | 10. The doneCallback() callback is fired (if provided) | class="my-animation" | 6089 * 6090 * @param {jQuery/jqLite element} element the element that will be the focus of the enter animation 6091 * @param {jQuery/jqLite element} parentElement the parent element of the element that will be the focus of the enter animation 6092 * @param {jQuery/jqLite element} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation 6093 * @param {function()=} doneCallback the callback function that will be called once the animation is complete 6094 */ 6095 enter : function(element, parentElement, afterElement, doneCallback) { 6096 this.enabled(false, element); 6097 $delegate.enter(element, parentElement, afterElement); 6098 $rootScope.$$postDigest(function() { 6099 performAnimation('enter', 'ng-enter', element, parentElement, afterElement, noop, doneCallback); 6100 }); 6101 }, 6102 6103 /** 6104 * @ngdoc function 6105 * @name ngAnimate.$animate#leave 6106 * @methodOf ngAnimate.$animate 6107 * @function 6108 *
6109 * @description 6110 * Runs the leave animation operation and, upon completion, removes the element from the DOM. Once 6111 * the animation is started, the following CSS classes will be added for the duration of the animation: 6112 * 6113 * Below is a breakdown of each step that occurs during leave animation: 6114 * 6115 * | Animation Step | What the element class attribute looks like | 6116 * |----------------------------------------------------------------------------------------------|---------------------------------------------| 6117 * | 1. $animate.leave(...) is called | class="my-animation" | 6118 * | 2. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | 6119 * | 3. the .ng-leave class is added to the element | class="my-animation ng-animate ng-leave" | 6120 * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-leave" | 6121 * | 5. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate ng-leave" | 6122 * | 6. the .ng-leave-active and .ng-animate-active classes is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active ng-leave ng-leave-active" | 6123 * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active ng-leave ng-leave-active" | 6124 * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | 6125 * | 9. The element is removed from the DOM | ... | 6126 * | 10. The doneCallback() callback is fired (if provided) | ... | 6127 * 6128 * @param {jQuery/jqLite element} element the element that will be the focus of the leave animation 6129 * @param {function()=} doneCallback the callback function that will be called once the animation is complete 6130 */ 6131 leave : function(element, doneCallback) { 6132 cancelChildAnimations(element); 6133 this.enabled(false, element); 6134 $rootScope.$$postDigest(function() { 6135 performAnimation('leave', 'ng-leave', element, null, null, function() { 6136 $delegate.leave(element); 6137 }, doneCallback); 6138 }); 6139 }, 6140 6141 /** 6142 * @ngdoc function 6143 * @name ngAnimate.$animate#move 6144 * @methodOf ngAnimate.$animate 6145 * @function 6146 * 6147 * @description 6148 * Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parentElement container or 6149 * add the element directly after the afterElement element if present. Then the move animation will be run. Once 6150 * the animation is started, the following CSS classes will be added for the duration of the animation: 6151 * 6152 * Below is a breakdown of each step that occurs during move animation: 6153 * 6154 * | Animation Step | What the element class attribute looks like | 6155 * |----------------------------------------------------------------------------------------------|---------------------------------------------| 6156 * | 1. $animate.move(...) is called | class="my-animation" | 6157 * | 2. element is moved into the parentElement element or beside the afterElement element | class="my-animation" | 6158 * | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | 6159 * | 4. the .ng-move class is added to the element | class="my-animation ng-animate ng-move" | 6160 * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-move" | 6161 * | 6. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate ng-move" | 6162 * | 7. the .ng-move-active and .ng-animate-active classes is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active ng-move ng-move-active" | 6163 * | 8. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active ng-move ng-move-active" | 6164 * | 9. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | 6165 * | 10. The doneCallback() callback is fired (if provided) | class="my-animation" | 6166 * 6167 * @param {jQuery/jqLite element} element the element that will be the focus of the move animation 6168 * @param {jQuery/jqLite element} parentElement the parentElement element of the element that will be the focus of the move animation 6169 * @param {jQuery/jqLite element} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation 6170 * @param {function()=} doneCallback the callback function that will be called once the animation is complete 6171 */ 6172 move : function(element, parentElement, afterElement, doneCallback) { 6173 cancelChildAnimations(element); 6174 this.enabled(false, element); 6175 $delegate.move(element, parentElement, afterElement); 6176 $rootScope.$$postDigest(function() { 6177 performAnimation('move', 'ng-move', element, parentElement, afterElement, noop, doneCallback); 6178 }); 6179 }, 6180 6181 /** 6182 * @ngdoc function 6183 * @name ngAnimate.$animate#addClass 6184 * @methodOf ngAnimate.$animate 6185 *
6186 * @description 6187 * Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class. 6188 * Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide 6189 * the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if no CSS transitions 6190 * or keyframes are defined on the -add or base CSS class). 6191 * 6192 * Below is a breakdown of each step that occurs during addClass animation: 6193 * 6194 * | Animation Step | What the element class attribute looks like | 6195 * |------------------------------------------------------------------------------------------------|---------------------------------------------| 6196 * | 1. $animate.addClass(element, 'super') is called | class="my-animation" | 6197 * | 2. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | 6198 * | 3. the .super-add class are added to the element | class="my-animation ng-animate super-add" | 6199 * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate super-add" | 6200 * | 5. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate super-add" | 6201 * | 6. the .super, .super-add-active and .ng-animate-active classes are added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active super super-add super-add-active" | 6202 * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation super-add super-add-active" | 6203 * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation super" | 6204 * | 9. The super class is kept on the element | class="my-animation super" | 6205 * | 10. The doneCallback() callback is fired (if provided) | class="my-animation super" | 6206 * 6207 * @param {jQuery/jqLite element} element the element that will be animated 6208 * @param {string} className the CSS class that will be added to the element and then animated 6209 * @param {function()=} doneCallback the callback function that will be called once the animation is complete 6210 */ 6211 addClass : function(element, className, doneCallback) { 6212 performAnimation('addClass', className, element, null, null, function() { 6213 $delegate.addClass(element, className); 6214 }, doneCallback); 6215 }, 6216 6217 /** 6218 * @ngdoc function 6219 * @name ngAnimate.$animate#removeClass 6220 * @methodOf ngAnimate.$animate 6221 * 6222 * @description 6223 * Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value 6224 * from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in 6225 * order to provide the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if 6226 * no CSS transitions or keyframes are defined on the -remove or base CSS classes). 6227 * 6228 * Below is a breakdown of each step that occurs during removeClass animation: 6229 * 6230 * | Animation Step | What the element class attribute looks like | 6231 * |-----------------------------------------------------------------------------------------------|---------------------------------------------| 6232 * | 1. $animate.removeClass(element, 'super') is called | class="my-animation super" | 6233 * | 2. $animate runs any JavaScript-defined animations on the element | class="my-animation super ng-animate" | 6234 * | 3. the .super-remove class are added to the element | class="my-animation super ng-animate super-remove"| 6235 * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation super ng-animate super-remove" | 6236 * | 5. $animate waits for 10ms (this performs a reflow) | class="my-animation super ng-animate super-remove" | 6237 * | 6. the .super-remove-active and .ng-animate-active classes are added and .super is removed (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active super-remove super-remove-active" | 6238 * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active super-remove super-remove-active" | 6239 * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | 6240 * | 9. The doneCallback() callback is fired (if provided) | class="my-animation" | 6241 * 6242 * 6243 * @param {jQuery/jqLite element} element the element that will be animated 6244 * @param {string} className the CSS class that will be animated and then removed from the element 6245 * @param {function()=} doneCallback the callback function that will be called once the animation is complete 6246 */ 6247 removeClass : function(element, className, doneCallback) { 6248 performAnimation('removeClass', className, element, null, null, function() { 6249 $delegate.removeClass(element, className); 6250 }, doneCallback); 6251 }, 6252 6253 /** 6254 * @ngdoc function 6255 * @name ngAnimate.$animate#enabled 6256 * @methodOf ngAnimate.$animate 6257 * @function 6258 * 6259 * @param {boolean=} value If provided then set the animation on or off. 6260 * @param {jQuery/jqLite element=} element If provided then the element will be used to represent the enable/disable operation 6261 * @return {boolean} Current animation state. 6262 *
6263 * @description 6264 * Globally enables/disables animations. 6265 * 6266 */ 6267 enabled : function(value, element) { 6268 switch(arguments.length) { 6269 case 2: 6270 if(value) { 6271 cleanup(element); 6272 } else { 6273 var data = element.data(NG_ANIMATE_STATE) || {}; 6274 data.disabled = true; 6275 element.data(NG_ANIMATE_STATE, data); 6276 } 6277 break; 6278 6279 case 1: 6280 rootAnimateState.disabled = !value; 6281 break; 6282 6283 default: 6284 value = !rootAnimateState.disabled; 6285 break; 6286 } 6287 return !!value; 6288 } 6289 }; 6290 6291 /* 6292 all animations call this shared animation triggering function internally. 6293 The animationEvent variable refers to the JavaScript animation event that will be triggered 6294 and the className value is the name of the animation that will be applied within the 6295 CSS code. Element, parentElement and afterElement are provided DOM elements for the animation 6296 and the onComplete callback will be fired once the animation is fully complete. 6297 */ 6298 function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) { 6299 var currentClassName, classes, node = extractElementNode(element); 6300 if(node) { 6301 currentClassName = node.className; 6302 classes = currentClassName + ' ' + className; 6303 } 6304 6305 //transcluded directives may sometimes fire an animation using only comment nodes 6306 //best to catch this early on to prevent any animation operations from occurring 6307 if(!node || !isAnimatableClassName(classes)) { 6308 fireDOMOperation(); 6309 closeAnimation(); 6310 return; 6311 } 6312 6313 var animationLookup = (' ' + classes).replace(/\s+/g,'.'); 6314 if (!parentElement) { 6315 parentElement = afterElement ? afterElement.parent() : element.parent(); 6316 } 6317 6318 var matches = lookup(animationLookup); 6319 var isClassBased = animationEvent == 'addClass' || animationEvent == 'removeClass'; 6320 var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; 6321 6322 //skip the animation if animations are disabled, a parent is already being animated, 6323 //the element is not currently attached to the document body or then completely close 6324 //the animation if any matching animations are not found at all. 6325 //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case a NO animation is not found. 6326 if (animationsDisabled(element, parentElement) || matches.length === 0) { 6327 fireDOMOperation(); 6328 closeAnimation(); 6329 return; 6330 } 6331 6332 var animations = []; 6333 //only add animations if the currently running animation is not structural 6334 //or if there is no animation running at all 6335 if(!ngAnimateState.running || !(isClassBased && ngAnimateState.structural)) { 6336 forEach(matches, function(animation) { 6337 //add the animation to the queue to if it is allowed to be cancelled 6338 if(!animation.allowCancel || animation.allowCancel(element, animationEvent, className)) { 6339 var beforeFn, afterFn = animation[animationEvent]; 6340 6341 //Special case for a leave animation since there is no point in performing an 6342 //animation on a element node that has already been removed from the DOM 6343 if(animationEvent == 'leave') { 6344 beforeFn = afterFn; 6345 afterFn = null; //this must be falsy so that the animation is skipped for leave 6346 } else { 6347 beforeFn = animation['before' + animationEvent.charAt(0).toUpperCase() + animationEvent.substr(1)]; 6348 } 6349 animations.push({ 6350 before : beforeFn, 6351 after : afterFn 6352 }); 6353 } 6354 }); 6355 } 6356 6357 //this would mean that an animation was not allowed so let the existing 6358 //animation do it's thing and close this one early 6359 if(animations.length === 0) { 6360 fireDOMOperation(); 6361 fireDoneCallbackAsync(); 6362 return; 6363 } 6364 6365 //this value will be searched for class-based CSS className lookup. Therefore, 6366 //we prefix and suffix the current className value with spaces to avoid substring 6367 //lookups of className tokens 6368 var futureClassName = ' ' + currentClassName + ' '; 6369 if(ngAnimateState.running) { 6370 //if an animation is currently running on the element then lets take the steps 6371 //to cancel that animation and fire any required callbacks 6372 $timeout.cancel(ngAnimateState.closeAnimationTimeout); 6373 cleanup(element); 6374 cancelAnimations(ngAnimateState.animations); 6375 6376 //if the class is removed during the reflow then it will revert the styles temporarily 6377 //back to the base class CSS styling causing a jump-like effect to occur. This check 6378 //here ensures that the domOperation is only performed after the reflow has commenced 6379 if(ngAnimateState.beforeComplete) { 6380 (ngAnimateState.done || noop)(true); 6381 } else if(isClassBased && !ngAnimateState.structural) { 6382 //class-based animations will compare element className values after cancelling the 6383 //previous animation to see if the element properties already contain the final CSS 6384 //class and if so then the animation will be skipped. Since the domOperation will 6385 //be performed only after the reflow is complete then our element's className value 6386 //will be invalid. Therefore the same string manipulation that would occur within the 6387 //DOM operation will be performed below so that the class comparison is valid... 6388 futureClassName = ngAnimateState.event == 'removeClass' ? 6389 futureClassName.replace(ngAnimateState.className, '')
6389: 6390 futureClassName + ngAnimateState.className + ' '; 6391 } 6392 } 6393 6394 //There is no point in perform a class-based animation if the element already contains 6395 //(on addClass) or doesn't contain (on removeClass) the className being animated. 6396 //The reason why this is being called after the previous animations are cancelled 6397 //is so that the CSS classes present on the element can be properly examined. 6398 var classNameToken = ' ' + className + ' '; 6399 if((animationEvent == 'addClass' && futureClassName.indexOf(classNameToken) >= 0) || 6400 (animationEvent == 'removeClass' && futureClassName.indexOf(classNameToken) == -1)) { 6401 fireDOMOperation(); 6402 fireDoneCallbackAsync(); 6403 return; 6404 } 6405 6406 //the ng-animate class does nothing, but it's here to allow for 6407 //parent animations to find and cancel child animations when needed 6408 element.addClass(NG_ANIMATE_CLASS_NAME); 6409 6410 element.data(NG_ANIMATE_STATE, { 6411 running:true, 6412 event:animationEvent, 6413 className:className, 6414 structural:!isClassBased, 6415 animations:animations, 6416 done:onBeforeAnimationsComplete 6417 }); 6418 6419 //first we run the before animations and when all of those are complete 6420 //then we perform the DOM operation and run the next set of animations 6421 invokeRegisteredAnimationFns(animations, 'before', onBeforeAnimationsComplete); 6422 6423 function onBeforeAnimationsComplete(cancelled) { 6424 fireDOMOperation(); 6425 if(cancelled === true) { 6426 closeAnimation(); 6427 return; 6428 } 6429 6430 //set the done function to the final done function 6431 //so that the DOM event won't be executed twice by accident 6432 //if the after animation is cancelled as well 6433 var data = element.data(NG_ANIMATE_STATE); 6434 if(data) { 6435 data.done = closeAnimation; 6436 element.data(NG_ANIMATE_STATE, data); 6437 } 6438 invokeRegisteredAnimationFns(animations, 'after', closeAnimation); 6439 } 6440 6441 function invokeRegisteredAnimationFns(animations, phase, allAnimationFnsComplete) { 6442 var endFnName = phase + 'End'; 6443 forEach(animations, function(animation, index) { 6444 var animationPhaseCompleted = function() { 6445 progress(index, phase); 6446 }; 6447 6448 //there are no before functions for enter + move since the DOM 6449 //operations happen before the performAnimation method fires 6450 if(phase == 'before' && (animationEvent == 'enter' || animationEvent == 'move')) { 6451 animationPhaseCompleted(); 6452 return; 6453 } 6454 6455 if(animation[phase]) { 6456 animation[endFnName] = isClassBased ? 6457 animation[phase](element, className, animationPhaseCompleted) : 6458 animation[phase](element, animationPhaseCompleted); 6459 } else { 6460 animationPhaseCompleted(); 6461 } 6462 }); 6463 6464 function progress(index, phase) { 6465 var phaseCompletionFlag = phase + 'Complete'; 6466 var currentAnimation = animations[index]; 6467 currentAnimation[phaseCompletionFlag] = true; 6468 (currentAnimation[endFnName] || noop)(); 6469 6470 for(var i=0;i<animations.length;i++) { 6471 if(!animations[i][phaseCompletionFlag]) return; 6472 } 6473 6474 allAnimationFnsComplete(); 6475 } 6476 } 6477 6478 function fireDoneCallbackAsync() { 6479 doneCallback && $timeout(doneCallback, 0, false); 6480 } 6481 6482 //it is less complicated to use a flag than managing and cancelling 6483 //timeouts containing multiple callbacks. 6484 function fireDOMOperation() { 6485 if(!fireDOMOperation.hasBeenRun) { 6486 fireDOMOperation.hasBeenRun = true; 6487 domOperation(); 6488 } 6489 } 6490 6491 function closeAnimation() { 6492 if(!closeAnimation.hasBeenRun) { 6493 closeAnimation.hasBeenRun = true; 6494 var data = element.data(NG_ANIMATE_STATE); 6495 if(data) { 6496 /* only structural animations wait for reflow before removing an 6497 animation, but class-based animations don't. An example of this 6498 failing would be when a parent HTML tag has a ng-class attribute 6499 causing ALL directives below to skip animations during the digest */ 6500 if(isClassBased) { 6501 cleanup(element); 6502 } else { 6503 data.closeAnimationTimeout = $timeout(function() { 6504 cleanup(element); 6505 }, 0, false); 6506 element.data(NG_ANIMATE_STATE, data); 6507 } 6508 } 6509 fireDoneCallbackAsync(); 6510 } 6511 } 6512 } 6513 6514 function cancelChildAnimations(element) { 6515 var node = extractElementNode(element);
6516 forEach(node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME), function(element) { 6517 element = angular.element(element); 6518 var data = element.data(NG_ANIMATE_STATE); 6519 if(data) { 6520 cancelAnimations(data.animations); 6521 cleanup(element); 6522 } 6523 }); 6524 } 6525 6526 function cancelAnimations(animations) { 6527 var isCancelledFlag = true; 6528 forEach(animations, function(animation) { 6529 if(!animations.beforeComplete) { 6530 (animation.beforeEnd || noop)(isCancelledFlag); 6531 } 6532 if(!animations.afterComplete) { 6533 (animation.afterEnd || noop)(isCancelledFlag); 6534 } 6535 }); 6536 } 6537 6538 function cleanup(element) { 6539 if(isMatchingElement(element, $rootElement)) { 6540 if(!rootAnimateState.disabled) { 6541 rootAnimateState.running = false; 6542 rootAnimateState.structural = false; 6543 } 6544 } else { 6545 element.removeClass(NG_ANIMATE_CLASS_NAME); 6546 element.removeData(NG_ANIMATE_STATE); 6547 } 6548 } 6549 6550 function animationsDisabled(element, parentElement) { 6551 if (rootAnimateState.disabled) return true; 6552 6553 if(isMatchingElement(element, $rootElement)) { 6554 return rootAnimateState.disabled || rootAnimateState.running; 6555 } 6556 6557 do { 6558 //the element did not reach the root element which means that it 6559 //is not apart of the DOM. Therefore there is no reason to do 6560 //any animations on it 6561 if(parentElement.length === 0) break; 6562 6563 var isRoot = isMatchingElement(parentElement, $rootElement); 6564 var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE); 6565 var result = state && (!!state.disabled || !!state.running); 6566 if(isRoot || result) { 6567 return result; 6568 } 6569 6570 if(isRoot) return true; 6571 } 6572 while(parentElement = parentElement.parent()); 6573 6574 return true; 6575 } 6576 }]); 6577 6578 $animateProvider.register('', ['$window', '$sniffer', '$timeout', function($window, $sniffer, $timeout) { 6579 // Detect proper transitionend/animationend event names. 6580 var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT; 6581 6582 // If unprefixed events are not supported but webkit-prefixed are, use the latter.
6583 // Otherwise, just use W3C names, browsers not supporting them at all will just ignore them. 6584 // Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend` 6585 // but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`. 6586 // Register both events in case `window.onanimationend` is not supported because of that, 6587 // do the same for `transitionend` as Safari is likely to exhibit similar behavior. 6588 // Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit 6589 // therefore there is no reason to test anymore for other vendor prefixes: http://caniuse.com/#search=transition 6590 if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) { 6591 CSS_PREFIX = '-webkit-'; 6592 TRANSITION_PROP = 'WebkitTransition'; 6593 TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; 6594 } else { 6595 TRANSITION_PROP = 'transition'; 6596 TRANSITIONEND_EVENT = 'transitionend'; 6597 } 6598 6599 if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) { 6600 CSS_PREFIX = '-webkit-'; 6601 ANIMATION_PROP = 'WebkitAnimation'; 6602 ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; 6603 } else { 6604 ANIMATION_PROP = 'animation'; 6605 ANIMATIONEND_EVENT = 'animationend'; 6606 } 6607 6608 var DURATION_KEY = 'Duration'; 6609 var PROPERTY_KEY = 'Property'; 6610 var DELAY_KEY = 'Delay'; 6611 var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; 6612 var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey'; 6613 var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data'; 6614 var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; 6615 var CLOSING_TIME_BUFFER = 1.5; 6616 var ONE_SECOND = 1000; 6617 6618 var animationCounter = 0; 6619 var lookupCache = {}; 6620 var parentCounter = 0; 6621 var animationReflowQueue = []; 6622 var animationElementQueue = []; 6623 var animationTimer; 6624 var closingAnimationTime = 0; 6625 var timeOut = false;
6626 function afterReflow(element, callback) { 6627 $timeout.cancel(animationTimer); 6628 6629 animationReflowQueue.push(callback); 6630 6631 var node = extractElementNode(element); 6632 element = angular.element(node); 6633 animationElementQueue.push(element); 6634 6635 var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); 6636 closingAnimationTime = Math.max(closingAnimationTime, 6637 (elementData.maxDelay + elementData.maxDuration) * CLOSING_TIME_BUFFER * ONE_SECOND); 6638 6639 //by placing a counter we can avoid an accidental 6640 //race condition which may close an animation when 6641 //a follow-up animation is midway in its animation 6642 elementData.animationCount = animationCounter; 6643 6644 animationTimer = $timeout(function() { 6645 forEach(animationReflowQueue, function(fn) { 6646 fn(); 6647 }); 6648 6649 //copy the list of elements so that successive 6650 //animations won't conflict if they're added before 6651 //the closing animation timeout has run 6652 var elementQueueSnapshot = []; 6653 var animationCounterSnapshot = animationCounter; 6654 forEach(animationElementQueue, function(elm) { 6655 elementQueueSnapshot.push(elm); 6656 }); 6657 6658 $timeout(function() { 6659 closeAllAnimations(elementQueueSnapshot, animationCounterSnapshot); 6660 elementQueueSnapshot = null; 6661 }, closingAnimationTime, false); 6662 6663 animationReflowQueue = []; 6664 animationElementQueue = []; 6665 animationTimer = null; 6666 lookupCache = {}; 6667 closingAnimationTime = 0; 6668 animationCounter++; 6669 }, 10, false); 6670 } 6671 6672 function closeAllAnimations(elements, count) { 6673 forEach(elements, function(element) { 6674 var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); 6675 if(elementData && elementData.animationCount == count) { 6676 (elementData.closeAnimationFn || noop)(); 6677 } 6678 }); 6679 } 6680 6681 function getElementAnimationDetails(element, cacheKey) { 6682 var data = cacheKey ? lookupCache[cacheKey] : null; 6683 if(!data) { 6684 var transitionDuration = 0; 6685 var transitionDelay = 0; 6686 var animationDuration = 0; 6687 var animationDelay = 0; 6688 var transitionDelayStyle; 6689 var animationDelayStyle; 6690 var transitionDurationStyle; 6691 var transitionPropertyStyle; 6692 6693 //we want all the styles defined before and after 6694 forEach(element, function(element) { 6695 if (element.nodeType == ELEMENT_NODE) { 6696 var elementStyles = $window.getComputedStyle(element) || {}; 6697 6698 transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY]; 6699 6700 transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration); 6701 6702 transitionPropertyStyle = elementStyles[TRANSITION_PROP + PROPERTY_KEY]; 6703 6704 transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY]; 6705 6706 transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay); 6707 6708 animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY]; 6709 6710 animationDelay = Math.max(parseMaxTime(animationDelayStyle), animationDelay); 6711 6712 var aDuration = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]); 6713 6714 if(aDuration > 0) { 6715 aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1; 6716 } 6717 6718 animationDuration = Math.max(aDuration, animationDuration); 6719 } 6720 }); 6721 data = { 6722 total : 0, 6723 transitionPropertyStyle: transitionPropertyStyle, 6724 transitionDurationStyle: transitionDurationStyle, 6725 transitionDelayStyle: transitionDelayStyle, 6726 transitionDelay: transitionDelay, 6727 transitionDuration: transitionDuration, 6728 animationDelayStyle: animationDelayStyle, 6729 animationDelay: animationDelay, 6730 animationDuration: animationDuration 6731 }; 6732 if(cacheKey) { 6733 lookupCache[cacheKey] = data; 6734 } 6735 } 6736 return data; 6737 } 6738 6739 function parseMaxTime(str) { 6740 var maxValue = 0; 6741 var values = angular.isString(str) ? 6742 str.split(/\s*,\s*/) : 6743 [];
6744 forEach(values, function(value) { 6745 maxValue = Math.max(parseFloat(value) || 0, maxValue); 6746 }); 6747 return maxValue; 6748 } 6749 6750 function getCacheKey(element) { 6751 var parentElement = element.parent(); 6752 var parentID = parentElement.data(NG_ANIMATE_PARENT_KEY); 6753 if(!parentID) { 6754 parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); 6755 parentID = parentCounter; 6756 } 6757 return parentID + '-' + extractElementNode(element).className; 6758 } 6759 6760 function animateSetup(element, className) { 6761 var cacheKey = getCacheKey(element); 6762 var eventCacheKey = cacheKey + ' ' + className; 6763 var stagger = {}; 6764 var ii = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0; 6765 6766 if(ii > 0) { 6767 var staggerClassName = className + '-stagger'; 6768 var staggerCacheKey = cacheKey + ' ' + staggerClassName; 6769 var applyClasses = !lookupCache[staggerCacheKey]; 6770 6771 applyClasses && element.addClass(staggerClassName); 6772 6773 stagger = getElementAnimationDetails(element, staggerCacheKey); 6774 6775 applyClasses && element.removeClass(staggerClassName); 6776 } 6777 6778 element.addClass(className); 6779 6780 var timings = getElementAnimationDetails(element, eventCacheKey); 6781 6782 /* there is no point in performing a reflow if the animation 6783 timeout is empty (this would cause a flicker bug normally 6784 in the page. There is also no point in performing an animation 6785 that only has a delay and no duration */ 6786 var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay); 6787 var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); 6788 if(maxDuration === 0) { 6789 element.removeClass(className); 6790 return false; 6791 } 6792 6793 //temporarily disable the transition so that the enter styles 6794 //don't animate twice (this is here to avoid a bug in Chrome/FF). 6795 var activeClassName = ''; 6796 timings.transitionDuration > 0 ? 6797 blockTransitions(element) : 6798 blockKeyframeAnimations(element); 6799 6800 forEach(className.split(' '), function(klass, i) { 6801 activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; 6802 }); 6803 6804 element.data(NG_ANIMATE_CSS_DATA_KEY, { 6805 className : className, 6806 activeClassName : activeClassName, 6807 maxDuration : maxDuration, 6808 maxDelay : maxDelay, 6809 classes : className + ' ' + activeClassName, 6810 timings : timings, 6811 stagger : stagger, 6812 ii : ii 6813 }); 6814 6815 return true; 6816 } 6817 6818 function blockTransitions(element) { 6819 extractElementNode(element).style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; 6820 } 6821 6822 function blockKeyframeAnimations(element) { 6823 extractElementNode(element).style[ANIMATION_PROP] = 'none 0s'; 6824 } 6825 6826 function unblockTransitions(element) { 6827 var prop = TRANSITION_PROP + PROPERTY_KEY; 6828 var node = extractElementNode(element); 6829 if(node.style[prop] && node.style[prop].length > 0) { 6830 node.style[prop] = ''; 6831 } 6832 } 6833 6834 function unblockKeyframeAnimations(element) { 6835 var prop = ANIMATION_PROP; 6836 var node = extractElementNode(element); 6837 if(node.style[prop] && node.style[prop].length > 0) { 6838 node.style[prop] = ''; 6839 } 6840 } 6841 6842 function animateRun(element, className, activeAnimationComplete) { 6843 var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); 6844 var node = extractElementNode(element); 6845 if(node.className.indexOf(className) == -1 || !elementData) { 6846 activeAnimationComplete(); 6847 return; 6848 } 6849 6850 var timings = elementData.timings; 6851 var stagger = elementData.stagger; 6852 var maxDuration = elementData.maxDuration; 6853 var activeClassName = elementData.activeClassName; 6854 var maxDelayTime = Math.max(timings.transitionDelay, timings.animationDelay) * ONE_SECOND; 6855 var startTime = Date.now(); 6856 var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; 6857 var ii = elementData.ii; 6858 6859 var style = '', appliedStyles = []; 6860 if(timings.transitionDuration > 0) { 6861 var propertyStyle = timings.transitionPropertyStyle; 6862 if(propertyStyle.indexOf('all') == -1) { 6863 style += CSS_PREFIX + 'transition-property: ' + propertyStyle + ';'; 6864 style += CSS_PREFIX + 'transition-duration: ' + timings.transitionDurationStyle + 's;'; 6865 appliedStyles.push(CSS_PREFIX + 'transition-property'); 6866 appliedStyles.push(CSS_PREFIX + 'transition-duration'); 6867 } 6868 } 6869 6870 if(ii > 0) { 6871 if(stagger.transitionDelay > 0 && stagger.transitionDuration === 0) { 6872 var delayStyle = timings.transitionDelayStyle; 6873 style += CSS_PREFIX + 'transition-delay: ' + 6874 prepareStaggerDelay(delayStyle, stagger.transitionDelay, ii) + '; '; 6875 appliedStyles.push(CSS_PREFIX + 'transition-delay'); 6876 } 6877 6878 if(stagger.animationDelay > 0 && stagger.animationDuration === 0) { 6879 style += CSS_PREFIX + 'animation-delay: ' + 6880 prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, ii) + '; '; 6881 appliedStyles.push(CSS_PREFIX + 'animation-delay'); 6882 } 6883 } 6884 6885 if(appliedStyles.length > 0) { 6886 //the element being animated may sometimes contain comment nodes in 6887 //the jqLite object, so we're safe to use a single variable to house 6888 //the styles since there is always only one element being animated 6889 var oldStyle = node.getAttribute('style') || ''; 6890 node.setAttribute('style', oldStyle + ' ' + style); 6891 } 6892 6893 element.on(css3AnimationEvents, onAnimationProgress); 6894 element.addClass(activeClassName); 6895 elementData.closeAnimationFn = function() { 6896 onEnd(); 6897 activeAnimationComplete(); 6898 }; 6899 return onEnd; 6900 6901 // This will automatically be called by $animate so 6902 // there is no need to attach this internally to the 6903 // timeout done method. 6904 function onEnd(cancelled) { 6905 element.off(css3AnimationEvents, onAnimationProgress); 6906 element.removeClass(activeClassName); 6907 animateClose(element, className); 6908 var node = extractElementNode(element); 6909 for (var i in appliedStyles) { 6910 node.style.removeProperty(appliedStyles[i]); 6911 } 6912 } 6913 6914 function onAnimationProgress(event) { 6915 event.stopPropagation(); 6916 var ev = event.originalEvent || event;
6917 var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now(); 6918 6919 /* Firefox (or possibly just Gecko) likes to not round values up 6920 * when a ms measurement is used for the animation */ 6921 var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); 6922 6923 /* $manualTimeStamp is a mocked timeStamp value which is set 6924 * within browserTrigger(). This is only here so that tests can 6925 * mock animations properly. Real events fallback to event.timeStamp, 6926 * or, if they don't, then a timeStamp is automatically created for them. 6927 * We're checking to see if the timeStamp surpasses the expected delay, 6928 * but we're using elapsedTime instead of the timeStamp on the 2nd 6929 * pre-condition since animations sometimes close off early */ 6930 if(Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { 6931 activeAnimationComplete(); 6932 } 6933 } 6934 } 6935 6936 function prepareStaggerDelay(delayStyle, staggerDelay, index) { 6937 var style = ''; 6938 forEach(delayStyle.split(','), function(val, i) { 6939 style += (i > 0 ? ',' : '') + 6940 (index * staggerDelay + parseInt(val, 10)) + 's'; 6941 }); 6942 return style; 6943 } 6944 6945 function animateBefore(element, className) { 6946 if(animateSetup(element, className)) { 6947 return function(cancelled) { 6948 cancelled && animateClose(element, className); 6949 }; 6950 } 6951 } 6952 6953 function animateAfter(element, className, afterAnimationComplete) { 6954 if(element.data(NG_ANIMATE_CSS_DATA_KEY)) { 6955 return animateRun(element, className, afterAnimationComplete); 6956 } else { 6957 animateClose(element, className); 6958 afterAnimationComplete(); 6959 } 6960 } 6961 6962 function animate(element, className, animationComplete) { 6963 //If the animateSetup function doesn't bother returning a 6964 //cancellation function then it means that there is no animation 6965 //to perform at all 6966 var preReflowCancellation = animateBefore(element, className); 6967 if(!preReflowCancellation) { 6968 animationComplete(); 6969 return; 6970 } 6971 6972 //There are two cancellation functions: one is before the first 6973 //reflow animation and the second is during the active state 6974 //animation. The first function will take care of removing the 6975 //data from the element which will not make the 2nd animation 6976 //happen in the first place 6977 var cancel = preReflowCancellation; 6978 afterReflow(element, function() { 6979 unblockTransitions(element); 6980 unblockKeyframeAnimations(element); 6981 //once the reflow is complete then we point cancel to 6982 //the new cancellation function which will remove all of the 6983 //animation properties from the active animation 6984 cancel = animateAfter(element, className, animationComplete); 6985 }); 6986 6987 return function(cancelled) { 6988 (cancel || noop)(cancelled); 6989 }; 6990 } 6991 6992 function animateClose(element, className) { 6993 element.removeClass(className); 6994 element.removeData(NG_ANIMATE_CSS_DATA_KEY); 6995 } 6996 6997 return { 6998 allowCancel : function(element, animationEvent, className) { 6999 //always cancel the current animation if it is a 7000 //structural animation 7001 var oldClasses = (element.data(NG_ANIMATE_CSS_DATA_KEY) || {}).classes; 7002 if(!oldClasses || ['enter','leave','move'].indexOf(animationEvent) >= 0) { 7003 return true; 7004 } 7005 7006 var parentElement = element.parent(); 7007 var clone = angular.element(extractElementNode(element).cloneNode()); 7008 7009 //make the element super hidden and override any CSS style values 7010 clone.attr('style','position:absolute; top:-9999px; left:-9999px'); 7011 clone.removeAttr('id'); 7012 clone.empty(); 7013 7014 forEach(oldClasses.split(' '), function(klass) { 7015 clone.removeClass(klass); 7016 }); 7017
7018 var suffix = animationEvent == 'addClass' ? '-add' : '-remove'; 7019 clone.addClass(suffixClasses(className, suffix)); 7020 parentElement.append(clone); 7021 7022 var timings = getElementAnimationDetails(clone); 7023 clone.remove(); 7024 7025 return Math.max(timings.transitionDuration, timings.animationDuration) > 0; 7026 }, 7027 7028 enter : function(element, animationCompleted) { 7029 return animate(element, 'ng-enter', animationCompleted); 7030 }, 7031 7032 leave : function(element, animationCompleted) { 7033 return animate(element, 'ng-leave', animationCompleted); 7034 }, 7035 7036 move : function(element, animationCompleted) { 7037 return animate(element, 'ng-move', animationCompleted); 7038 }, 7039 7040 beforeAddClass : function(element, className, animationCompleted) { 7041 var cancellationMethod = animateBefore(element, suffixClasses(className, '-add')); 7042 if(cancellationMethod) { 7043 afterReflow(element, function() { 7044 unblockTransitions(element); 7045 unblockKeyframeAnimations(element); 7046 animationCompleted(); 7047 }); 7048 return cancellationMethod; 7049 } 7050 animationCompleted(); 7051 }, 7052 7053 addClass : function(element, className, animationCompleted) { 7054 return animateAfter(element, suffixClasses(className, '-add'), animationCompleted); 7055 }, 7056 7057 beforeRemoveClass : function(element, className, animationCompleted) { 7058 var cancellationMethod = animateBefore(element, suffixClasses(className, '-remove')); 7059 if(cancellationMethod) { 7060 afterReflow(element, function() { 7061 unblockTransitions(element); 7062 unblockKeyframeAnimations(element); 7063 animationCompleted(); 7064 }); 7065 return cancellationMethod; 7066 } 7067 animationCompleted(); 7068 }, 7069 7070 removeClass : function(element, className, animationCompleted) { 7071 return animateAfter(element, suffixClasses(className, '-remove'), animationCompleted); 7072 } 7073 }; 7074 7075 function suffixClasses(classes, suffix) { 7076 var className = ''; 7077 classes = angular.isArray(classes) ? classes : classes.split(/\s+/); 7078 forEach(classes, function(klass, i) { 7079 if(klass && klass.length > 0) { 7080 className += (i > 0 ? ' ' : '') + klass + suffix; 7081 } 7082 }); 7083 return className; 7084 } 7085 }]); 7086 }]); 7087 7088 7089})(window, window.angular); 7090 7091/** 7092 * @license AngularJS v1.2.7 7093 * (c) 2010-2014 Google, Inc. http://angularjs.org 7094 * License: MIT 7095 */ 7096(function(window, angular, undefined) {'use strict'; 7097 7098/** 7099 * @ngdoc overview 7100 * @name ngTouch 7101 * @description 7102 * 7103 * # ngTouch 7104 * 7105 * The `ngTouch` module provides touch events and other helpers for touch-enabled devices. 7106 * The implementation is based on jQuery Mobile touch event handling 7107 * ([jquerymobile.com](http://jquerymobile.com/)). 7108 * 7109 * {@installModule touch} 7110 * 7111 * See {@link ngTouch.$swipe `$swipe`} for usage. 7112 * 7113 * <div doc-module-components="ngTouch"></div> 7114 * 7115 */ 7116 7117// define ngTouch module 7118/* global -ngTouch */ 7119var ngTouch = angular.module('ngTouch', []); 7120 7121/* global ngTouch: false */ 7122 7123 /** 7124 * @ngdoc object 7125 * @name ngTouch.$swipe 7126 * 7127 * @description 7128 * The `$swipe` service is a service that abstracts the messier details of hold-and-drag swipe 7129 * behavior, to make implementing swipe-related directives more convenient. 7130 * 7131 * Requires the {@link ngTouch `ngTouch`} module to be installed. 7132 * 7133 * `$swipe` is used by the `ngSwipeLeft` and `ngSwipeRight` directives in `ngTouch`, and by 7134 * `ngCarousel` in a separate component. 7135 * 7136 * # Usage 7137 * The `$swipe` service is an object with a single method: `bind`. `bind` takes an element 7138 * which is to be watched for swipes, and an object with four handler functions. See the 7139 * documentation for `bind` below. 7140 */ 7141 7142ngTouch.factory('$swipe', [function() { 7143 // The total distance in any direction before we make the call on swipe vs. scroll. 7144 var MOVE_BUFFER_RADIUS = 10; 7145 7146 function getCoordinates(event) { 7147 var touches = event.touches && event.touches.length ? event.touches : [event]; 7148 var e = (event.changedTouches && event.changedTouches[0]) || 7149 (event.originalEvent && event.originalEvent.changedTouches && 7150 event.originalEvent.changedTouches[0]) || 7151 touches[0].originalEvent || touches[0]; 7152 7153 return { 7154 x: e.clientX, 7155 y: e.clientY 7156 }; 7157 } 7158 7159 return { 7160 /** 7161 * @ngdoc method 7162 * @name ngTouch.$swipe#bind 7163 * @methodOf ngTouch.$swipe 7164 *
7165 * @description 7166 * The main method of `$swipe`. It takes an element to be watched for swipe motions, and an 7167 * object containing event handlers. 7168 * 7169 * The four events are `start`, `move`, `end`, and `cancel`. `start`, `move`, and `end` 7170 * receive as a parameter a coordinates object of the form `{ x: 150, y: 310 }`. 7171 * 7172 * `start` is called on either `mousedown` or `touchstart`. After this event, `$swipe` is 7173 * watching for `touchmove` or `mousemove` events. These events are ignored until the total 7174 * distance moved in either dimension exceeds a small threshold. 7175 * 7176 * Once this threshold is exceeded, either the horizontal or vertical delta is greater. 7177 * - If the horizontal distance is greater, this is a swipe and `move` and `end` events follow. 7178 * - If the vertical distance is greater, this is a scroll, and we let the browser take over. 7179 * A `cancel` event is sent. 7180 * 7181 * `move` is called on `mousemove` and `touchmove` after the above logic has determined that 7182 * a swipe is in progress. 7183 * 7184 * `end` is called when a swipe is successfully completed with a `touchend` or `mouseup`. 7185 * 7186 * `cancel` is called either on a `touchcancel` from the browser, or when we begin scrolling 7187 * as described above. 7188 * 7189 */ 7190 bind: function(element, eventHandlers) { 7191 // Absolute total movement, used to control swipe vs. scroll. 7192 var totalX, totalY; 7193 // Coordinates of the start position. 7194 var startCoords; 7195 // Last event's position. 7196 var lastPos; 7197 // Whether a swipe is active. 7198 var active = false; 7199 7200 element.on('touchstart mousedown', function(event) { 7201 startCoords = getCoordinates(event); 7202 active = true; 7203 totalX = 0; 7204 totalY = 0; 7205 lastPos = startCoords; 7206 eventHandlers['start'] && eventHandlers['start'](startCoords, event); 7207 }); 7208 7209 element.on('touchcancel', function(event) { 7210 active = false; 7211 eventHandlers['cancel'] && eventHandlers['cancel'](event); 7212 }); 7213 7214 element.on('touchmove mousemove', function(event) { 7215 if (!active) return; 7216 7217 // Android will send a touchcancel if it thinks we're starting to scroll. 7218 // So when the total distance (+ or - or both) exceeds 10px in either direction, 7219 // we either: 7220 // - On totalX > totalY, we send preventDefault() and treat this as a swipe. 7221 // - On totalY > totalX, we let the browser handle it as a scroll. 7222 7223 if (!startCoords) return; 7224 var coords = getCoordinates(event); 7225 7226 totalX += Math.abs(coords.x - lastPos.x); 7227 totalY += Math.abs(coords.y - lastPos.y); 7228 7229 lastPos = coords; 7230 7231 if (totalX < MOVE_BUFFER_RADIUS && totalY < MOVE_BUFFER_RADIUS) { 7232 return; 7233 } 7234 7235 // One of totalX or totalY has exceeded the buffer, so decide on swipe vs. scroll. 7236 if (totalY > totalX) { 7237 // Allow native scrolling to take over. 7238 active = false; 7239 eventHandlers['cancel'] && eventHandlers['cancel'](event); 7240 return; 7241 } else { 7242 // Prevent the browser from scrolling. 7243 event.preventDefault(); 7244 eventHandlers['move'] && eventHandlers['move'](coords, event); 7245 } 7246 }); 7247 7248 element.on('touchend mouseup', function(event) { 7249 if (!active) return; 7250 active = false; 7251 eventHandlers['end'] && eventHandlers['end'](getCoordinates(event), event); 7252 }); 7253 } 7254 }; 7255}]); 7256 7257/* global ngTouch: false */ 7258 7259/** 7260 * @ngdoc directive 7261 * @name ngTouch.directive:ngClick 7262 * 7263 * @description 7264 * A more powerful replacement for the default ngClick designed to be used on touchscreen 7265 * devices. Most mobile browsers wait about 300ms after a tap-and-release before sending 7266 * the click event. This version handles them immediately, and then prevents the 7267 * following click event from propagating. 7268 * 7269 * Requires the {@link ngTouch `ngTouch`} module to be installed. 7270 * 7271 * This directive can fall back to using an ordinary click event, and so works on desktop 7272 * browsers as well as mobile. 7273 * 7274 * This directive also sets the CSS class `ng-click-active` while the element is being held 7275 * down (by a mouse click or touch) so you can restyle the depressed element if you wish. 7276 * 7277 * @element ANY 7278 * @param {expression} ngClick {@link guide/expression Expression} to evaluate 7279 * upon tap. (Event object is available as `$event`) 7280 * 7281 * @example 7282 <doc:example> 7283 <doc:source> 7284 <button ng-click="count = count + 1" ng-init="count=0"> 7285 Increment 7286 </button> 7287 count: {{ count }} 7288 </doc:source> 7289 </doc:example> 7290 */ 7291 7292ngTouch.config(['$provide', function($provide) { 7293 $provide.decorator('ngClickDirective', ['$delegate', function($delegate) { 7294 // drop the default ngClick directive 7295 $delegate.shift(); 7296 return $delegate; 7297 }]); 7298}]); 7299 7300ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', 7301 function($parse, $timeout, $rootElement) { 7302 var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag. 7303 var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers. 7304 var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click 7305 var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks.
7306 7307 var ACTIVE_CLASS_NAME = 'ng-click-active'; 7308 var lastPreventedTime; 7309 var touchCoordinates; 7310 7311 7312 // TAP EVENTS AND GHOST CLICKS 7313 // 7314 // Why tap events? 7315 // Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're 7316 // double-tapping, and then fire a click event. 7317 // 7318 // This delay sucks and makes mobile apps feel unresponsive. 7319 // So we detect touchstart, touchmove, touchcancel and touchend ourselves and determine when 7320 // the user has tapped on something. 7321 // 7322 // What happens when the browser then generates a click event? 7323 // The browser, of course, also detects the tap and fires a click after a delay. This results in 7324 // tapping/clicking twice. So we do "clickbusting" to prevent it. 7325 // 7326 // How does it work? 7327 // We attach global touchstart and click handlers, that run during the capture (early) phase. 7328 // So the sequence for a tap is: 7329 // - global touchstart: Sets an "allowable region" at the point touched. 7330 // - element's touchstart: Starts a touch 7331 // (- touchmove or touchcancel ends the touch, no click follows) 7332 // - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold 7333 // too long) and fires the user's tap handler. The touchend also calls preventGhostClick(). 7334 // - preventGhostClick() removes the allowable region the global touchstart created. 7335 // - The browser generates a click event. 7336 // - The global click handler catches the click, and checks whether it was in an allowable region. 7337 // - If preventGhostClick was called, the region will have been removed, the click is busted. 7338 // - If the region is still there, the click proceeds normally. Therefore clicks on links and 7339 // other elements without ngTap on them work normally. 7340 // 7341 // This is an ugly, terrible hack! 7342 // Yeah, tell me about it. The alternatives are using the slow click events, or making our users 7343 // deal with the ghost clicks, so I consider this the least of evils. Fortunately Angular 7344 // encapsulates this ugly logic away from the user. 7345 // 7346 // Why not just put click handlers on the element? 7347 // We do that too, just to be sure. The problem is that the tap event might have caused the DOM 7348 // to change, so that the click fires in the same position but something else is there now. So 7349 // the handlers are global and care only about coordinates and not elements. 7350 7351 // Checks if the coordinates are close enough to be within the region. 7352 function hit(x1, y1, x2, y2) { 7353 return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD; 7354 } 7355 7356 // Checks a list of allowable regions against a click location. 7357 // Returns true if the click should be allowed. 7358 // Splices out the allowable region from the list after it has been used. 7359 function checkAllowableRegions(touchCoordinates, x, y) { 7360 for (var i = 0; i < touchCoordinates.length; i += 2) { 7361 if (hit(touchCoordinates[i], touchCoordinates[i+1], x, y)) { 7362 touchCoordinates.splice(i, i + 2); 7363 return true; // allowable region 7364 } 7365 } 7366 return false; // No allowable region; bust it. 7367 } 7368 7369 // Global click handler that prevents the click if it's in a bustable zone and preventGhostClick 7370 // was called recently. 7371 function onClick(event) { 7372 if (Date.now() - lastPreventedTime > PREVENT_DURATION) { 7373 return; // Too old. 7374 } 7375 7376 var touches = event.touches && event.touches.length ? event.touches : [event]; 7377 var x = touches[0].clientX; 7378 var y = touches[0].clientY; 7379 // Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label 7380 // and on the input element). Depending on the exact browser, this second click we don't want 7381 // to bust has either (0,0) or negative coordinates. 7382 if (x < 1 && y < 1) { 7383 return; // offscreen 7384 } 7385 7386 // Look for an allowable region containing this click. 7387 // If we find one, that means it was created by touchstart and not removed by 7388 // preventGhostClick, so we don't bust it. 7389 if (checkAllowableRegions(touchCoordinates, x, y)) { 7390 return; 7391 } 7392 7393 // If we didn't find an allowable region, bust the click. 7394 event.stopPropagation(); 7395 event.preventDefault(); 7396 7397 // Blur focused form elements 7398 event.target && event.target.blur(); 7399 } 7400 7401 7402 // Global touchstart handler that creates an allowable region for a click event. 7403 // This allowable region can be removed by preventGhostClick if we want to bust it. 7404 function onTouchStart(event) { 7405 var touches = event.touches && event.touches.length ? event.touches : [event]; 7406 var x = touches[0].clientX; 7407 var y = touches[0].clientY; 7408 touchCoordinates.push(x, y); 7409 7410 $timeout(function() { 7411 // Remove the allowable region. 7412 for (var i = 0; i < touchCoordinates.length; i += 2) { 7413 if (touchCoordinates[i] == x && touchCoordinates[i+1] == y) { 7414 touchCoordinates.splice(i, i + 2); 7415 return; 7416 } 7417 } 7418 }, PREVENT_DURATION, false); 7419 } 7420 7421 // On the first call, attaches some event handlers. Then whenever it gets called, it creates a 7422 // zone around the touchstart where clicks will get busted. 7423 function preventGhostClick(x, y) { 7424 if (!touchCoordinates) { 7425 $rootElement[0].addEventListener('click', onClick, true); 7426 $rootElement[0].addEventListener('touchstart', onTouchStart, true); 7427 touchCoordinates = []; 7428 } 7429 7430 lastPreventedTime = Date.now(); 7431 7432 checkAllowableRegions(touchCoordinates, x, y); 7433 } 7434 7435 // Actual linking function. 7436 return function(scope, element, attr) { 7437 var clickHandler = $parse(attr.ngClick), 7438 tapping = false, 7439 tapElement, // Used to blur the element after a tap. 7440 startTime, // Used to check if the tap was held too long. 7441 touchStartX, 7442 touchStartY; 7443 7444 function resetState() { 7445 tapping = false;
7446 element.removeClass(ACTIVE_CLASS_NAME); 7447 } 7448 7449 element.on('touchstart', function(event) { 7450 tapping = true; 7451 tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement. 7452 // Hack for Safari, which can target text nodes instead of containers. 7453 if(tapElement.nodeType == 3) { 7454 tapElement = tapElement.parentNode; 7455 } 7456 7457 element.addClass(ACTIVE_CLASS_NAME); 7458 7459 startTime = Date.now(); 7460 7461 var touches = event.touches && event.touches.length ? event.touches : [event]; 7462 var e = touches[0].originalEvent || touches[0]; 7463 touchStartX = e.clientX; 7464 touchStartY = e.clientY; 7465 }); 7466 7467 element.on('touchmove', function(event) { 7468 resetState(); 7469 }); 7470 7471 element.on('touchcancel', function(event) { 7472 resetState(); 7473 }); 7474 7475 element.on('touchend', function(event) { 7476 var diff = Date.now() - startTime; 7477 7478 var touches = (event.changedTouches && event.changedTouches.length) ? event.changedTouches : 7479 ((event.touches && event.touches.length) ? event.touches : [event]); 7480 var e = touches[0].originalEvent || touches[0]; 7481 var x = e.clientX; 7482 var y = e.clientY; 7483 var dist = Math.sqrt( Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2) ); 7484 7485 if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) { 7486 // Call preventGhostClick so the clickbuster will catch the corresponding click. 7487 preventGhostClick(x, y); 7488 7489 // Blur the focused element (the button, probably) before firing the callback. 7490 // This doesn't work perfectly on Android Chrome, but seems to work elsewhere. 7491 // I couldn't get anything to work reliably on Android Chrome. 7492 if (tapElement) { 7493 tapElement.blur(); 7494 } 7495 7496 if (!angular.isDefined(attr.disabled) || attr.disabled === false) { 7497 element.triggerHandler('click', [event]); 7498 } 7499 } 7500 7501 resetState(); 7502 }); 7503 7504 // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click 7505 // something else nearby. 7506 element.onclick = function(event) { }; 7507 7508 // Actual click handler. 7509 // There are three different kinds of clicks, only two of which reach this point. 7510 // - On desktop browsers without touch events, their clicks will always come here. 7511 // - On mobile browsers, the simulated "fast" click will call this. 7512 // - But the browser's follow-up slow click will be "busted" before it reaches this handler. 7513 // Therefore it's safe to use this directive on both mobile and desktop. 7514 element.on('click', function(event, touchend) { 7515 scope.$apply(function() { 7516 clickHandler(scope, {$event: (touchend || event)}); 7517 }); 7518 }); 7519 7520 element.on('mousedown', function(event) { 7521 element.addClass(ACTIVE_CLASS_NAME); 7522 }); 7523 7524 element.on('mousemove mouseup', function(event) { 7525 element.removeClass(ACTIVE_CLASS_NAME); 7526 }); 7527 7528 }; 7529}]); 7530 7531/* global ngTouch: false */ 7532 7533/** 7534 * @ngdoc directive 7535 * @name ngTouch.directive:ngSwipeLeft 7536 * 7537 * @description 7538 * Specify custom behavior when an element is swiped to the left on a touchscreen device. 7539 * A leftward swipe is a quick, right-to-left slide of the finger. 7540 * Though ngSwipeLeft is designed for touch-based devices, it will work with a mouse click and drag 7541 * too. 7542 * 7543 * Requires the {@link ngTouch `ngTouch`} module to be installed. 7544 * 7545 * @element ANY 7546 * @param {expression} ngSwipeLeft {@link guide/expression Expression} to evaluate 7547 * upon left swipe. (Event object is available as `$event`) 7548 * 7549 * @example 7550 <doc:example> 7551 <doc:source> 7552 <div ng-show="!showActions" ng-swipe-left="showActions = true"> 7553 Some list content, like an email in the inbox 7554 </div> 7555 <div ng-show="showActions" ng-swipe-right="showActions = false"> 7556 <button ng-click="reply()">Reply</button> 7557 <button ng-click="delete()">Delete</button> 7558 </div> 7559 </doc:source> 7560 </doc:example> 7561 */ 7562 7563/** 7564 * @ngdoc directive 7565 * @name ngTouch.directive:ngSwipeRight 7566 * 7567 * @description 7568 * Specify custom behavior when an element is swiped to the right on a touchscreen device. 7569 * A rightward swipe is a quick, left-to-right slide of the finger. 7570 * Though ngSwipeRight is designed for touch-based devices, it will work with a mouse click and drag 7571 * too. 7572 * 7573 * Requires the {@link ngTouch `ngTouch`} module to be installed. 7574 * 7575 * @element ANY 7576 * @param {expression} ngSwipeRight {@link guide/expression Expression} to evaluate 7577 * upon right swipe. (Event object is available as `$event`) 7578 * 7579 * @example 7580 <doc:example> 7581 <doc:source> 7582 <div ng-show="!showActions" ng-swipe-left="showActions = true"> 7583 Some list content, like an email in the inbox 7584 </div> 7585 <div ng-show="showActions" ng-swipe-right="showActions = false"> 7586 <button ng-click="reply()">Reply</button> 7587 <button ng-click="delete()">Delete</button> 7588 </div> 7589 </doc:source> 7590 </doc:example> 7591 */ 7592 7593function makeSwipeDirective(directiveName, direction, eventName) { 7594 ngTouch.directive(directiveName, ['$parse', '$swipe', function($parse, $swipe) { 7595 // The maximum vertical delta for a swipe should be less than 75px. 7596 var MAX_VERTICAL_DISTANCE = 75; 7597 // Vertical distance should not be more than a fraction of the horizontal distance. 7598 var MAX_VERTICAL_RATIO = 0.3; 7599 // At least a 30px lateral motion is necessary for a swipe. 7600 var MIN_HORIZONTAL_DISTANCE = 30; 7601 7602 return function(scope, element, attr) { 7603 var swipeHandler = $parse(attr[directiveName]); 7604 7605 var startCoords, valid; 7606 7607 function validSwipe(coords) { 7608 // Check that it's within the coordinates. 7609 // Absolute vertical distance must be within tolerances. 7610 // Horizontal distance, we take the current X - the starting X. 7611 // This is negative for leftward swipes and positive for rightward swipes. 7612 // After multiplying by the direction (-1 for left, +1 for right), legal swipes 7613 // (ie. same direction as the directive wants) will have a positive delta and 7614 // illegal ones a negative delta. 7615 // Therefore this delta must be positive, and larger than the minimum. 7616 if (!startCoords) return false;
7617 var deltaY = Math.abs(coords.y - startCoords.y); 7618 var deltaX = (coords.x - startCoords.x) * direction; 7619 return valid && // Short circuit for already-invalidated swipes. 7620 deltaY < MAX_VERTICAL_DISTANCE && 7621 deltaX > 0 && 7622 deltaX > MIN_HORIZONTAL_DISTANCE && 7623 deltaY / deltaX < MAX_VERTICAL_RATIO; 7624 } 7625 7626 $swipe.bind(element, { 7627 'start': function(coords, event) { 7628 startCoords = coords; 7629 valid = true; 7630 }, 7631 'cancel': function(event) { 7632 valid = false; 7633 }, 7634 'end': function(coords, event) { 7635 if (validSwipe(coords)) { 7636 scope.$apply(function() { 7637 element.triggerHandler(eventName); 7638 swipeHandler(scope, {$event: event}); 7639 }); 7640 } 7641 } 7642 }); 7643 }; 7644 }]); 7645} 7646 7647// Left is negative X-coordinate, right is positive. 7648makeSwipeDirective('ngSwipeLeft', -1, 'swipeleft'); 7649makeSwipeDirective('ngSwipeRight', 1, 'swiperight'); 7650 7651 7652 7653})(window, window.angular); 7654 7655/*! 7656* screenfull 7657* v1.1.1 - 2013-11-20 7658* https://github.com/sindresorhus/screenfull.js 7659* (c) Sindre Sorhus; MIT License 7660*/ 7661/*global Element */ 7662(function (window, document) { 7663 'use strict'; 7664 7665 var keyboardAllowed = typeof Element !== 'undefined' && 'ALLOW_KEYBOARD_INPUT' in Element, // IE6 throws without typeof check 7666 7667 fn = (function () { 7668 var val, valLength; 7669 var fnMap = [ 7670 [ 7671 'requestFullscreen', 7672 'exitFullscreen', 7673 'fullscreenElement', 7674 'fullscreenEnabled', 7675 'fullscreenchange', 7676 'fullscreenerror' 7677 ], 7678 // new WebKit 7679 [ 7680 'webkitRequestFullscreen', 7681 'webkitExitFullscreen', 7682 'webkitFullscreenElement', 7683 'webkitFullscreenEnabled', 7684 'webkitfullscreenchange', 7685 'webkitfullscreenerror' 7686 7687 ], 7688 // old WebKit (Safari 5.1) 7689 [ 7690 'webkitRequestFullScreen', 7691 'webkitCancelFullScreen', 7692 'webkitCurrentFullScreenElement', 7693 'webkitCancelFullScreen', 7694 'webkitfullscreenchange', 7695 'webkitfullscreenerror' 7696 7697 ], 7698 [ 7699 'mozRequestFullScreen', 7700 'mozCancelFullScreen', 7701 'mozFullScreenElement', 7702 'mozFullScreenEnabled', 7703 'mozfullscreenchange', 7704 'mozfullscreenerror' 7705 ], 7706 [ 7707 'msRequestFullscreen', 7708 'msExitFullscreen', 7709 'msFullscreenElement', 7710 'msFullscreenEnabled', 7711 'MSFullscreenChange', 7712 'MSFullscreenError' 7713 ] 7714 ]; 7715 var i = 0; 7716 var l = fnMap.length; 7717 var ret = {}; 7718 7719 for (; i < l; i++) { 7720 val = fnMap[i]; 7721 if (val && val[1] in document) { 7722 for (i = 0, valLength = val.length; i < valLength; i++) { 7723 ret[fnMap[0][i]] = val[i]; 7724 } 7725 return ret; 7726 } 7727 } 7728 return false; 7729 })(), 7730 7731 screenfull = { 7732 request: function (elem) { 7733 var request = fn.requestFullscreen; 7734 7735 elem = elem || document.documentElement; 7736 7737 // Work around Safari 5.1 bug: reports support for 7738 // keyboard in fullscreen even though it doesn't. 7739 // Browser sniffing, since the alternative with 7740 // setTimeout is even worse. 7741 if (/5\.1[\.\d]* Safari/.test(navigator.userAgent)) { 7742 elem[request](); 7743 } else { 7744 elem[request](keyboardAllowed && Element.ALLOW_KEYBOARD_INPUT); 7745 } 7746 }, 7747 exit: function () { 7748 document[fn.exitFullscreen](); 7749 }, 7750 toggle: function (elem) { 7751 if (this.isFullscreen) { 7752 this.exit(); 7753 } else { 7754 this.request(elem); 7755 } 7756 }, 7757 onchange: function () {}, 7758 onerror: function () {}, 7759 raw: fn 7760 }; 7761 7762 if (!fn) { 7763 window.screenfull = false; 7764 return; 7765 } 7766 7767 Object.defineProperties(screenfull, { 7768 isFullscreen: { 7769 get: function () { 7770 return !!document[fn.fullscreenElement]; 7771 } 7772 }, 7773 element: { 7774 enumerable: true, 7775 get: function () { 7776 return document[fn.fullscreenElement]; 7777 } 7778 }, 7779 enabled: { 7780 enumerable: true, 7781 get: function () { 7782 // Coerce to boolean in case of old WebKit 7783 return !!document[fn.fullscreenEnabled]; 7784 } 7785 } 7786 }); 7787 7788 document.addEventListener(fn.fullscreenchange, function (e) { 7789 screenfull.onchange.call(screenfull, e); 7790 }); 7791 7792 document.addEventListener(fn.fullscreenerror, function (e) { 7793 screenfull.onerror.call(screenfull, e); 7794 }); 7795 7796 window.screenfull = screenfull; 7797})(window, document); 7798 7799/* 7800CryptoJS v3.1.2 7801code.google.com/p/crypto-js
vendor: 5,229 bytes, lines 7802-7820
7802(c) 2009-2013 by Jeff Mott. All rights reserved. 7803code.google.com/p/crypto-js/wiki/License 7804*/ 7805var CryptoJS=CryptoJS||function(h,s){var f={},g=f.lib={},q=function(){},m=g.Base={extend:function(a){q.prototype=this;var c=new q;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, 7806r=g.WordArray=m.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||k).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e<a;e++)c[b+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)c[b+e>>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< 780732-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=m.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d<a;d+=4)c.push(4294967296*h.random()|0);return new r.init(c,a)}}),l=f.enc={},k=l.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++){var e=c[b>>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b+=2)d[b>>>3]|=parseInt(a.substr(b, 78082),16)<<24-4*(b%8);return new r.init(d,c/2)}},n=l.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++)d.push(String.fromCharCode(c[b>>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b++)d[b>>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new r.init(d,c)}},j=l.Utf8={stringify:function(a){try{return decodeURIComponent(escape(n.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return n.parse(unescape(encodeURIComponent(a)))}}, 7809u=g.BufferedBlockAlgorithm=m.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var g=0;g<a;g+=e)this._doProcessBlock(d,g);g=d.splice(0,a);c.sigBytes-=b}return new r.init(g,b)},clone:function(){var a=m.clone.call(this); 7810a._data=this._data.clone();return a},_minBufferSize:0});g.Hasher=u.extend({cfg:m.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){u.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(c,d){return(new a.init(d)).finalize(c)}},_createHmacHelper:function(a){return function(c,d){return(new t.HMAC.init(a, 7811d)).finalize(c)}}});var t=f.algo={};return f}(Math); 7812(function(h){for(var s=CryptoJS,f=s.lib,g=f.WordArray,q=f.Hasher,f=s.algo,m=[],r=[],l=function(a){return 4294967296*(a-(a|0))|0},k=2,n=0;64>n;){var j;a:{j=k;for(var u=h.sqrt(j),t=2;t<=u;t++)if(!(j%t)){j=!1;break a}j=!0}j&&(8>n&&(m[n]=l(h.pow(k,0.5))),r[n]=l(h.pow(k,1/3)),n++);k++}var a=[],f=f.SHA256=q.extend({_doReset:function(){this._hash=new g.init(m.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],g=b[2],j=b[3],h=b[4],m=b[5],n=b[6],q=b[7],p=0;64>p;p++){if(16>p)a[p]= 7813c[d+p]|0;else{var k=a[p-15],l=a[p-2];a[p]=((k<<25|k>>>7)^(k<<14|k>>>18)^k>>>3)+a[p-7]+((l<<15|l>>>17)^(l<<13|l>>>19)^l>>>10)+a[p-16]}k=q+((h<<26|h>>>6)^(h<<21|h>>>11)^(h<<7|h>>>25))+(h&m^~h&n)+r[p]+a[p];l=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&g^f&g);q=n;n=m;m=h;h=j+k|0;j=g;g=f;f=e;e=k+l|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+g|0;b[3]=b[3]+j|0;b[4]=b[4]+h|0;b[5]=b[5]+m|0;b[6]=b[6]+n|0;b[7]=b[7]+q|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes; 7814d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=q.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=q._createHelper(f);s.HmacSHA256=q._createHmacHelper(f)})(Math); 7815(function(){var h=CryptoJS,s=h.enc.Utf8;h.algo.HMAC=h.lib.Base.extend({init:function(f,g){f=this._hasher=new f.init;"string"==typeof g&&(g=s.parse(g));var h=f.blockSize,m=4*h;g.sigBytes>m&&(g=f.finalize(g));g.clamp();for(var r=this._oKey=g.clone(),l=this._iKey=g.clone(),k=r.words,n=l.words,j=0;j<h;j++)k[j]^=1549556828,n[j]^=909522486;r.sigBytes=l.sigBytes=m;this.reset()},reset:function(){var f=this._hasher;f.reset();f.update(this._iKey)},update:function(f){this._hasher.update(f);return this},finalize:function(f){var g= 7816this._hasher;f=g.finalize(f);g.reset();return g.finalize(this._oKey.clone().concat(f))}})})(); 7817 7818/* 7819CryptoJS v3.1.2 7820code.google.com/p/crypto-js
7821(c) 2009-2013 by Jeff Mott. All rights reserved. 7822code.google.com/p/crypto-js/wiki/License 7823*/ 7824(function () { 7825 // Shortcuts 7826 var C = CryptoJS; 7827 var C_lib = C.lib; 7828 var WordArray = C_lib.WordArray; 7829 var C_enc = C.enc; 7830 7831 /** 7832 * Base64 encoding strategy. 7833 */ 7834 var Base64 = C_enc.Base64 = { 7835 /** 7836 * Converts a word array to a Base64 string. 7837 * 7838 * @param {WordArray} wordArray The word array. 7839 * 7840 * @return {string} The Base64 string. 7841 * 7842 * @static 7843 * 7844 * @example 7845 * 7846 * var base64String = CryptoJS.enc.Base64.stringify(wordArray); 7847 */ 7848 stringify: function (wordArray) { 7849 // Shortcuts 7850 var words = wordArray.words; 7851 var sigBytes = wordArray.sigBytes; 7852 var map = this._map; 7853 7854 // Clamp excess bits 7855 wordArray.clamp(); 7856 7857 // Convert 7858 var base64Chars = []; 7859 for (var i = 0; i < sigBytes; i += 3) { 7860 var byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; 7861 var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; 7862 var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; 7863 7864 var triplet = (byte1 << 16) | (byte2 << 8) | byte3; 7865 7866 for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) { 7867 base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f)); 7868 } 7869 } 7870 7871 // Add padding 7872 var paddingChar = map.charAt(64); 7873 if (paddingChar) { 7874 while (base64Chars.length % 4) { 7875 base64Chars.push(paddingChar); 7876 } 7877 } 7878 7879 return base64Chars.join(''); 7880 }, 7881 7882 /** 7883 * Converts a Base64 string to a word array. 7884 * 7885 * @param {string} base64Str The Base64 string. 7886 * 7887 * @return {WordArray} The word array. 7888 * 7889 * @static 7890 * 7891 * @example 7892 * 7893 * var wordArray = CryptoJS.enc.Base64.parse(base64String); 7894 */ 7895 parse: function (base64Str) { 7896 // Shortcuts 7897 var base64StrLength = base64Str.length; 7898 var map = this._map; 7899 7900 // Ignore padding 7901 var paddingChar = map.charAt(64); 7902 if (paddingChar) { 7903 var paddingIndex = base64Str.indexOf(paddingChar); 7904 if (paddingIndex != -1) { 7905 base64StrLength = paddingIndex; 7906 } 7907 } 7908 7909 // Convert 7910 var words = []; 7911 var nBytes = 0; 7912 for (var i = 0; i < base64StrLength; i++) { 7913 if (i % 4) { 7914 var bits1 = map.indexOf(base64Str.charAt(i - 1)) << ((i % 4) * 2); 7915 var bits2 = map.indexOf(base64Str.charAt(i)) >>> (6 - (i % 4) * 2); 7916 words[nBytes >>> 2] |= (bits1 | bits2) << (24 - (nBytes % 4) * 8); 7917 nBytes++; 7918 } 7919 } 7920 7921 return WordArray.create(words, nBytes); 7922 }, 7923 7924 _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' 7925 }; 7926}()); 7927 7928/* ng-infinite-scroll - v1.0.0 - 2013-02-23 */ 7929var mod; 7930 7931mod = angular.module('infinite-scroll', []); 7932 7933mod.directive('infiniteScroll', [ 7934 '$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) { 7935 return { 7936 link: function(scope, elem, attrs) { 7937 var checkWhenEnabled, handler, scrollDistance, scrollEnabled; 7938 $window = angular.element($window); 7939 scrollDistance = 0; 7940 if (attrs.infiniteScrollDistance != null) { 7941 scope.$watch(attrs.infiniteScrollDistance, function(value) { 7942 return scrollDistance = parseInt(value, 10); 7943 }); 7944 } 7945 scrollEnabled = true; 7946 checkWhenEnabled = false; 7947 if (attrs.infiniteScrollDisabled != null) { 7948 scope.$watch(attrs.infiniteScrollDisabled, function(value) { 7949 scrollEnabled = !value; 7950 if (scrollEnabled && checkWhenEnabled) { 7951 checkWhenEnabled = false;
7952 return handler(); 7953 } 7954 }); 7955 } 7956 handler = function() { 7957 var elementBottom, remaining, shouldScroll, windowBottom; 7958 windowBottom = $window.height() + $window.scrollTop(); 7959 elementBottom = elem.offset().top + elem.height(); 7960 remaining = elementBottom - windowBottom; 7961 shouldScroll = remaining <= $window.height() * scrollDistance; 7962 if (shouldScroll && scrollEnabled) { 7963 if ($rootScope.$$phase) { 7964 return scope.$eval(attrs.infiniteScroll); 7965 } else { 7966 return scope.$apply(attrs.infiniteScroll); 7967 } 7968 } else if (shouldScroll) { 7969 return checkWhenEnabled = true; 7970 } 7971 }; 7972 $window.on('scroll', handler); 7973 scope.$on('$destroy', function() { 7974 return $window.off('scroll', handler); 7975 }); 7976 return $timeout((function() { 7977 if (attrs.infiniteScrollImmediateCheck) { 7978 if (scope.$eval(attrs.infiniteScrollImmediateCheck)) { 7979 return handler(); 7980 } 7981 } else { 7982 return handler(); 7983 } 7984 }), 0); 7985 } 7986 }; 7987 } 7988]); 7989 7990/** 7991 * @license Angulartics v0.8.5 7992 * (c) 2013 Luis Farzati http://luisfarzati.github.io/angulartics 7993 * License: MIT 7994 */ 7995(function(angular, analytics) { 7996'use strict'; 7997 7998var angulartics = window.angulartics || (window.angulartics = {}); 7999angulartics.waitForVendorApi = function (objectName, delay, registerFn) { 8000 if (!Object.prototype.hasOwnProperty.call(window, objectName)) { 8001 setTimeout(function () { angulartics.waitForVendorApi(objectName, delay, registerFn); }, delay); 8002 } 8003 else { 8004 registerFn(window[objectName]); 8005 } 8006}; 8007 8008/** 8009 * @ngdoc overview 8010 * @name angulartics 8011 */ 8012angular.module('angulartics', []) 8013.provider('$analytics', function () { 8014 var settings = { 8015 pageTracking: { 8016 autoTrackFirstPage: true, 8017 autoTrackVirtualPages: true, 8018 basePath: '', 8019 bufferFlushDelay: 1000 8020 }, 8021 eventTracking: { 8022 bufferFlushDelay: 1000 8023 } 8024 }; 8025 8026 var cache = { 8027 pageviews: [], 8028 events: [] 8029 }; 8030 8031 var bufferedPageTrack = function (path) { 8032 cache.pageviews.push(path); 8033 }; 8034 var bufferedEventTrack = function (event, properties) { 8035 cache.events.push({name: event, properties: properties}); 8036 }; 8037 8038 var api = { 8039 settings: settings, 8040 pageTrack: bufferedPageTrack, 8041 eventTrack: bufferedEventTrack 8042 }; 8043 8044 var registerPageTrack = function (fn) { 8045 api.pageTrack = fn; 8046 angular.forEach(cache.pageviews, function (path, index) { 8047 setTimeout(function () { api.pageTrack(path); }, index * settings.pageTracking.bufferFlushDelay); 8048 }); 8049 }; 8050 var registerEventTrack = function (fn) { 8051 api.eventTrack = fn; 8052 angular.forEach(cache.events, function (event, index) { 8053 setTimeout(function () { api.eventTrack(event.name, event.properties); }, index * settings.eventTracking.bufferFlushDelay); 8054 }); 8055 }; 8056 8057 return { 8058 $get: function() { return api; }, 8059 settings: settings, 8060 virtualPageviews: function (value) { this.settings.pageTracking.autoTrackVirtualPages = value; }, 8061 firstPageview: function (value) { this.settings.pageTracking.autoTrackFirstPage = value; }, 8062 withBase: function (value) { this.settings.pageTracking.basePath = (value) ? angular.element('base').attr('href').slice(0, -1) : ''; }, 8063 registerPageTrack: registerPageTrack, 8064 registerEventTrack: registerEventTrack 8065 }; 8066}) 8067 8068.run(['$rootScope', '$location', '$analytics', function ($rootScope, $location, $anal
8068ytics) { 8069 if ($analytics.settings.pageTracking.autoTrackFirstPage) { 8070 $analytics.pageTrack($location.absUrl()); 8071 } 8072 if ($analytics.settings.pageTracking.autoTrackVirtualPages) { 8073 $rootScope.$on('$routeChangeSuccess', function (event, current) { 8074 if (current && (current.$$route||current).redirectTo) return; 8075 var url = $analytics.settings.pageTracking.basePath + $location.url(); 8076 $analytics.pageTrack(url); 8077 }); 8078 } 8079}]) 8080 8081.directive('analyticsOn', ['$analytics', function ($analytics) { 8082 function isCommand(element) { 8083 return ['a:','button:','button:button','button:submit','input:button','input:submit'].indexOf( 8084 element.tagName.toLowerCase()+':'+(element.type||'')) >= 0; 8085 } 8086 8087 function inferEventType(element) { 8088 if (isCommand(element)) return 'click'; 8089 return 'click'; 8090 } 8091 8092 function inferEventName(element) { 8093 if (isCommand(element)) return element.innerText || element.value; 8094 return element.id || element.name || element.tagName; 8095 } 8096 8097 function isProperty(name) { 8098 return name.substr(0, 9) === 'analytics' && ['on', 'event'].indexOf(name.substr(10)) === -1; 8099 } 8100 8101 return { 8102 restrict: 'A', 8103 scope: false, 8104 link: function ($scope, $element, $attrs) { 8105 var eventType = $attrs.analyticsOn || inferEventType($element[0]), 8106 eventName = $attrs.analyticsEvent || inferEventName($element[0]); 8107 8108 var properties = {}; 8109 angular.forEach($attrs.$attr, function(attr, name) { 8110 if (isProperty(attr)) { 8111 properties[name.slice(9).toLowerCase()] = $attrs[name]; 8112 } 8113 }); 8114 8115 angular.element($element[0]).bind(eventType, function () { 8116 $analytics.eventTrack(eventName, properties); 8117 }); 8118 } 8119 }; 8120}]); 8121})(angular); 8122 8123/** 8124 * @license Angulartics v0.8.5 8125 * (c) 2013 Luis Farzati http://luisfarzati.github.io/angulartics 8126 * Universal Analytics update contributed by http://github.com/willmcclellan 8127 * License: MIT 8128 */ 8129(function(angular) { 8130'use strict'; 8131 8132/** 8133 * @ngdoc overview 8134 * @name angulartics.google.analytics 8135 * Enables analytics support for Google Analytics (http://google.com/analytics) 8136 */ 8137angular.module('angulartics.google.analytics', ['angulartics']) 8138.config(['$analyticsProvider', function ($analyticsProvider) { 8139 8140 // GA already supports buffered invocations so we don't need 8141 // to wrap these inside angulartics.waitForVendorApi 8142 8143 $analyticsProvider.registerPageTrack(function (path) { 8144 if (window._gaq) _gaq.push(['_trackPageview', path]); 8145 if (window.ga) ga('send', 'pageview', path); 8146 }); 8147 8148 $analyticsProvider.registerEventTrack(function (action, properties) { 8149 if (window._gaq) _gaq.push(['_trackEvent', properties.category, action, properties.label, properties.value]); 8150 if (window.ga) ga('send', 'event', properties.category, action, properties.label, properties.value); 8151 }); 8152 8153}]); 8154})(angular); 8155/*global angular*/ 8156angular.module('ezfb', []) 8157 8158.provider('$FB', function () { 8159 8160 var NO_CALLBACK = -1; 8161 8162 /** 8163 * Specify published apis and executable callback argument index 8164 * 8165 * ref: https://developers.facebook.com/docs/reference/javascript/ 8166 */ 8167 var _publishedApis = { 8168 // core 8169 api: [1, 2, 3], 8170 ui: 1, 8171 8172 // auth 8173 // getAuthResponse: 0, // deprecated 8174 getLoginStatus: 0, 8175 login: 0, 8176 logout: 0, 8177 8178 // event 8179 'Event.subscribe': 1, 8180 'Event.unsubscribe': 1, // not quite a callback though 8181 8182 // xfbml 8183 'XFBML.parse': NO_CALLBACK, 8184 8185 // canvas 8186 'Canvas.Prefetcher.addStaticResource': NO_CALLBACK, 8187 'Canvas.Prefetcher.setCollectionMode': NO_CALLBACK, 8188 'Canvas.hideFlashElement': NO_CALLBACK, 8189 'Canvas.scrollTo': NO_CALLBACK, 8190 'Canvas.setAutoGrow': NO_CALLBACK, 8191 'Canvas.setDoneLoading': 0, 8192 'Canvas.setSize': NO_CALLBACK, 8193 'Canvas.setUrlHandler': 0, 8194 'Canvas.showFlashElement': NO_CALLBACK, 8195 'Canvas.startTimer': NO_CALLBACK, 8196 'Canvas.stopTimer': 0 8197 }; 8198 8199 // Default locale 8200 var _locale = 'en_US'; 8201 8202 // Default init parameters 8203 var _initParams = { 8204 // appId : '', // App ID from the App Dashboard 8205 // channelUrl : '', // Channel File for x-domain communication 8206 status : true, // check the login status upon init? 8207 cookie : true, // set sessions cookies to allow your server to access the session? 8208 xfbml : true // parse XFBML tags on this page? 8209 }; 8210 8211 // Default init function 8212 var _defaultInitFunction = ['$window', '$fbInitParams', function ($window, $fbInitParams) { 8213 // Initialize the FB JS SDK 8214 $window.FB.init($fbInitParams); 8215 }]; 8216 var _initFunction = _defaultInitFunction; 8217 8218 /** 8219 * Generate namespace route in an object 8220 * 8221 * @param {object} obj target object 8222 * @param {array} paths ordered path asc 8223 */ 8224 function _pathGen(obj, paths) { 8225 if (paths.length === 0) { 8226 return; 8227 } 8228 var path = paths.shift(); 8229 if (!obj[path]) { 8230 obj[path] = {}; 8231 } 8232 _pathGen(obj, paths); 8233 } 8234 8235 /** 8236 * Getter/setter of a config 8237 * 8238 * @param {object} target to be configured object 8239 * @param {object} config configuration(optional) 8240 * @return {*} copied target if "config" is not given 8241 */ 8242 function _config(target, config) { 8243 if (angular.isObject(config)) { 8244 angular.extend(target, config); 8245 } 8246 else { 8247 return angular.copy(target); 8248 } 8249 } 8250 8251 /** 8252 * Context and arguments proxy function 8253 * 8254 * @param {function} func the function 8255 * @param {object} context the context 8256 * @param {array} args arguments 8257 * @return {function} proxied function 8258 */
8259 function _proxy(func, context, args) { 8260 return function () { 8261 return func.apply(context, args); 8262 }; 8263 } 8264 8265 return { 8266 //////////////////////////// 8267 // provider configuration // 8268 //////////////////////////// 8269 setInitParams: function (params) { 8270 _config(_initParams, params); 8271 }, 8272 getInitParams: function () { 8273 return _config(_initParams); 8274 }, 8275 8276 setLocale: function(locale) { 8277 _locale = locale; 8278 }, 8279 getLocale: function() { 8280 return _locale; 8281 }, 8282 8283 setInitFunction: function (func) { 8284 if (angular.isArray(func) || angular.isFunction(func)) { 8285 _initFunction = func; 8286 } 8287 else { 8288 throw new Error('Init function type error.'); 8289 } 8290 }, 8291 getInitFunction: function () { 8292 return _initFunction; 8293 }, 8294 8295 ////////// 8296 // $get // 8297 ////////// 8298 $get: [ 8299 '$window', '$q', '$document', '$parse', '$rootScope', '$injector', 8300 function ($window, $q, $document, $parse, $rootScope, $injector) { 8301 var _initReady, _$FB, 8302 _savedListeners = {}; 8303 8304 var _paramsReady = $q.defer(); 8305 8306 if (_initParams.appId || _initFunction !== _defaultInitFunction) { 8307 _paramsReady.resolve(); 8308 } 8309 8310 /** 8311 * #fb-root check & create 8312 */ 8313 if (!$document[0].getElementById('fb-root')) { 8314 $document.find('body').append('<div id="fb-root"></div>'); 8315 } 8316 8317 _initReady = $q.defer(); 8318 // Load the SDK's source Asynchronously 8319 (function(d){ 8320 var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0]; 8321 if (d.getElementById(id)) {return;} 8322 js = d.createElement('script'); js.id = id; js.async = true; 8323 js.src = "//connect.facebook.net/" + _locale + "/all.js"; 8324 ref.parentNode.insertBefore(js, ref); 8325 }($document[0])); 8326 8327 $window.fbAsyncInit = function () { 8328 _paramsReady.promise.then(function() { 8329 // Run init function 8330 $injector.invoke(_initFunction, null, {'$fbInitParams': _initParams}); 8331 8332 _$FB.$$ready = true; 8333 _initReady.resolve(); 8334 }); 8335 }; 8336 8337 _$FB = { 8338 $$ready: false, 8339 init: function (params) { 8340 _config(_initParams, params); 8341 _paramsReady.resolve(); 8342 } 8343 }; 8344 8345 /** 8346 * _$FB initialization 8347 * 8348 * Publish FB APIs with auto-check ready state 8349 */ 8350 angular.forEach(_publishedApis, function (cbArgIndex, apiPath) { 8351 _pathGen(_$FB, apiPath.split(/\./)); 8352 8353 var getter = $parse(apiPath), 8354 setter = getter.assign; 8355 setter(_$FB, function () { 8356 8357 var args = Array.prototype.slice.call(arguments), 8358 apiCall = _proxy(function (args) { 8359 var dfd = $q.defer(), 8360 /** 8361 * Add or replce original callback function with deferred resolve 8362 * 8363 * @param {number} index expected api callback index 8364 */ 8365 replaceCallbackAt = function (index) { 8366 var func = angular.isFunction(args[index]) ? args[index] : angular.noop, 8367 newFunc = function () { 8368 var funcArgs = Array.prototype.slice.call(arguments); 8369 8370 if ($rootScope.$$phase) { 8371 // already in angularjs context 8372 func.apply(null, funcArgs); 8373 dfd.resolve.apply(dfd, funcArgs); 8374 } 8375 else { 8376 // not in angularjs context 8377 $rootScope.$apply(function () { 8378 func.apply(null, funcArgs); 8379 dfd.resolve.apply(dfd, funcArgs); 8380 }); 8381 } 8382 }; 8383 8384 while (args.length <= index) { 8385 args.push(null); 8386 } 8387 8388 /** 8389 * `FB.Event.unsubscribe` requires the original listener function. 8390 * Save the mapping of original->wrapped on `FB.Event.subscribe` for unsubscribing. 8391 */ 8392 var eventName;
8393 if (apiPath === 'Event.subscribe') { 8394 eventName = args[0]; 8395 if (angular.isUndefined(_savedListeners[eventName])) { 8396 _savedListeners[eventName] = []; 8397 } 8398 _savedListeners[eventName].push({ 8399 original: func, 8400 wrapped: newFunc 8401 }); 8402 } 8403 else if (apiPath === 'Event.unsubscribe') { 8404 eventName = args[0]; 8405 if (angular.isArray(_savedListeners[eventName])) { 8406 var i, subscribed, l = _savedListeners[eventName].length; 8407 for (i = 0; i < l; i++) { 8408 subscribed = _savedListeners[eventName][i]; 8409 if (subscribed.original === func) { 8410 newFunc = subscribed.wrapped; 8411 _savedListeners[eventName].splice(i, 1); 8412 break; 8413 } 8414 } 8415 } 8416 } 8417 8418 // Replace the original one (or null) with newFunc 8419 args[index] = newFunc; 8420 }; 8421 8422 if (cbArgIndex !== NO_CALLBACK) { 8423 if (angular.isNumber(cbArgIndex)) { 8424 /** 8425 * Constant callback argument index 8426 */ 8427 replaceCallbackAt(cbArgIndex); 8428 } 8429 else if (angular.isArray(cbArgIndex)) { 8430 /** 8431 * Multiple possible callback argument index 8432 */ 8433 var i, c; 8434 for (i = 0; i < cbArgIndex.length; i++) { 8435 c = cbArgIndex[i]; 8436 8437 if (args.length == c || 8438 args.length == (c + 1) && angular.isFunction(args[c])) { 8439 8440 replaceCallbackAt(c); 8441 8442 break; 8443 } 8444 } 8445 } 8446 } 8447 8448 /** 8449 * Apply back to original FB SDK 8450 */ 8451 var origFBFunc = getter($window.FB); 8452 if (!origFBFunc) { 8453 throw new Error("Facebook API `FB." + apiPath + "` doesn't exist."); 8454 } 8455 origFBFunc.apply($window.FB, args); 8456 8457 return dfd.promise; 8458 }, null, [args]); 8459 8460 /** 8461 * Wrap the api function with our ready promise 8462 */ 8463 if (cbArgIndex === NO_CALLBACK) { 8464 // Do not return promise for no-callback apis 8465 _initReady.promise.then(apiCall); 8466 } 8467 else { 8468 return _initReady.promise.then(apiCall); 8469 } 8470 }); 8471 }); 8472 8473 return _$FB; 8474 }] 8475 }; 8476}) 8477 8478/** 8479 * @ngdoc directive 8480 * @name ng.directive:ezfbXfbml 8481 * @restrict EAC 8482 * 8483 * @description 8484 * Parse XFBML inside the directive 8485 * 8486 * @param {boolean} ezfb-xfbml Reload trigger for inside XFBML, 8487 * should keep only XFBML content inside the directive. 8488 * @param {expr} onrender Evaluated every time content xfbml gets rendered. 8489 */ 8490.directive('ezfbXfbml', [ 8491 '$FB', '$parse', '$compile', '$timeout', 8492function ($FB, $parse, $compile, $timeout) { 8493 return { 8494 restrict: 'EAC', 8495 compile: function (tElm, tAttrs) { 8496 var _savedHtml = tElm.html(); 8497 8498 return function postLink(scope, iElm, iAttrs) { 8499 var _rendering = true, 8500 onrenderExp = iAttrs.onrender || '', 8501 onrenderHandler = function () { 8502 if (_rendering) { 8503 scope.$eval(onrenderExp); 8504 _rendering = false; 8505 } 8506 }, 8507 renderEvent = 'xfbml.render'; 8508 8509 /** 8510 * Render event 8511 */ 8512 if (onrenderExp) { 8513 // Subscribe 8514 $FB.Event.subscribe(renderEvent, onrenderHandler); 8515 8516 // Unsubscibe on $destroy 8517 iElm.bind('$destroy', function () { 8518 $FB.Event.unsubscribe(renderEvent, onrenderHandler); 8519 8520 iElm.unbind('$destroy'); 8521 }); 8522 } 8523 8524 $FB.XFBML.parse(iElm[0]); 8525 8526 /** 8527 * The trigger 8528 */ 8529 var setter = $parse(iAttrs.ezfbXfbml).assign; 8530 scope.$watch(iAttrs.ezfbXfbml, function (val) { 8531 if (val) { 8532 _rendering = true; 8533 iElm.html(_savedHtml); 8534 8535 $compile(iElm.contents())(scope); 8536 $timeout(function () { 8537 $FB.XFBML.parse(iElm[0]); 8538 }); 8539 8540 // Reset the trigger if it's settable 8541 (setter || angular.noop)(scope, false); 8542 } 8543 }, true); 8544 8545 }; 8546 } 8547 }; 8548}]); 8549 8550/** 8551 * @license AngularJS v1.2.6 8552 * (c) 2010-2014 Google, Inc. http://angularjs.org 8553 * License: MIT 8554 */ 8555(function(window, angular, undefined) {'use strict'; 8556 8557/** 8558 * @ngdoc overview 8559 * @name ngCookies 8560 * @description 8561 * 8562 * # ngCookies 8563 * 8564 * The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies. 8565 * 8566 * {@installModule cookies} 8567 * 8568 * <div doc-module-components="ngCookies"></div> 8569 * 8570 * See {@link ngCookies.$cookies `$cookies`} and 8571 * {@link ngCookies.$cookieStore `$cookieStore`} for usage. 8572 */ 8573 8574 8575angular.module('ngCookies', ['ng']). 8576 /** 8577 * @ngdoc object 8578 * @name ngCookies.$cookies 8579 * @requires $browser 8580 * 8581 * @description 8582 * Provides read/write access to browser's cookies. 8583 * 8584 * Only a simple Object is exposed and by adding or removing properties to/from 8585 * this object, new cookies are created/deleted at the end of current $eval. 8586 * 8587 * Requires the {@link ngCookies `ngCookies`} module to be installed. 8588 * 8589 * @example 8590 <doc:example> 8591 <doc:source> 8592 <script>
8593 function ExampleController($cookies) { 8594 // Retrieving a cookie 8595 var favoriteCookie = $cookies.myFavorite; 8596 // Setting a cookie 8597 $cookies.myFavorite = 'oatmeal'; 8598 } 8599 </script> 8600 </doc:source> 8601 </doc:example> 8602 */ 8603 factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) { 8604 var cookies = {}, 8605 lastCookies = {}, 8606 lastBrowserCookies, 8607 runEval = false, 8608 copy = angular.copy, 8609 isUndefined = angular.isUndefined; 8610 8611 //creates a poller fn that copies all cookies from the $browser to service & inits the service 8612 $browser.addPollFn(function() { 8613 var currentCookies = $browser.cookies(); 8614 if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl 8615 lastBrowserCookies = currentCookies; 8616 copy(currentCookies, lastCookies); 8617 copy(currentCookies, cookies); 8618 if (runEval) $rootScope.$apply(); 8619 } 8620 })(); 8621 8622 runEval = true; 8623 8624 //at the end of each eval, push cookies 8625 //TODO: this should happen before the "delayed" watches fire, because if some cookies are not 8626 // strings or browser refuses to store some cookies, we update the model in the push fn. 8627 $rootScope.$watch(push); 8628 8629 return cookies; 8630 8631 8632 /** 8633 * Pushes all the cookies from the service to the browser and verifies if all cookies were 8634 * stored. 8635 */ 8636 function push() { 8637 var name, 8638 value, 8639 browserCookies, 8640 updated; 8641 8642 //delete any cookies deleted in $cookies 8643 for (name in lastCookies) { 8644 if (isUndefined(cookies[name])) { 8645 $browser.cookies(name, undefined); 8646 } 8647 } 8648 8649 //update all cookies updated in $cookies 8650 for(name in cookies) { 8651 value = cookies[name]; 8652 if (!angular.isString(value)) { 8653 if (angular.isDefined(lastCookies[name])) { 8654 cookies[name] = lastCookies[name]; 8655 } else { 8656 delete cookies[name]; 8657 } 8658 } else if (value !== lastCookies[name]) { 8659 $browser.cookies(name, value); 8660 updated = true; 8661 } 8662 } 8663 8664 //verify what was actually stored 8665 if (updated){ 8666 updated = false; 8667 browserCookies = $browser.cookies(); 8668 8669 for (name in cookies) { 8670 if (cookies[name] !== browserCookies[name]) { 8671 //delete or reset all cookies that the browser dropped from $cookies 8672 if (isUndefined(browserCookies[name])) { 8673 delete cookies[name]; 8674 } else { 8675 cookies[name] = browserCookies[name]; 8676 } 8677 updated = true; 8678 } 8679 } 8680 } 8681 } 8682 }]). 8683 8684 8685 /** 8686 * @ngdoc object 8687 * @name ngCookies.$cookieStore 8688 * @requires $cookies 8689 * 8690 * @description 8691 * Provides a key-value (string-object) storage, that is backed by session cookies. 8692 * Objects put or retrieved from this storage are automatically serialized or 8693 * deserialized by angular's toJson/fromJson. 8694 * 8695 * Requires the {@link ngCookies `ngCookies`} module to be installed. 8696 * 8697 * @example 8698 */ 8699 factory('$cookieStore', ['$cookies', function($cookies) { 8700 8701 return { 8702 /** 8703 * @ngdoc method 8704 * @name ngCookies.$cookieStore#get 8705 * @methodOf ngCookies.$cookieStore 8706 * 8707 * @description 8708 * Returns the value of given cookie key 8709 * 8710 * @param {string} key Id to use for lookup. 8711 * @returns {Object} Deserialized cookie value. 8712 */ 8713 get: function(key) { 8714 var value = $cookies[key]; 8715 return value ? angular.fromJson(value) : value; 8716 }, 8717 8718 /** 8719 * @ngdoc method 8720 * @name ngCookies.$cookieStore#put 8721 * @methodOf ngCookies.$cookieStore 8722 * 8723 * @description 8724 * Sets a value for given cookie key 8725 * 8726 * @param {string} key Id for the `value`. 8727 * @param {Object} value Value to be stored. 8728 */ 8729 put: function(key, value) { 8730 $cookies[key] = angular.toJson(value); 8731 }, 8732 8733 /** 8734 * @ngdoc method 8735 * @name ngCookies.$cookieStore#remove 8736 * @methodOf ngCookies.$cookieStore 8737 *
8738 * @description 8739 * Remove given cookie 8740 * 8741 * @param {string} key Id of the key-value pair to delete. 8742 */ 8743 remove: function(key) { 8744 delete $cookies[key]; 8745 } 8746 }; 8747 8748 }]); 8749 8750 8751})(window, window.angular);
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.