github.com/abayer/test-infra@v0.0.5/mungegithub/submit-queue/www/angular-material.js (about) 1 /*! 2 * Angular Material Design 3 * https://github.com/angular/material 4 * @license MIT 5 * v1.0.7 6 */ 7 (function( window, angular, undefined ){ 8 "use strict"; 9 10 (function(){ 11 "use strict"; 12 13 angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.gestures","material.core.layout","material.core.theming.palette","material.core.theming","material.core.animate","material.components.autocomplete","material.components.backdrop","material.components.button","material.components.bottomSheet","material.components.card","material.components.checkbox","material.components.content","material.components.chips","material.components.datepicker","material.components.dialog","material.components.divider","material.components.fabActions","material.components.fabShared","material.components.fabSpeedDial","material.components.fabToolbar","material.components.fabTrigger","material.components.gridList","material.components.icon","material.components.input","material.components.menu","material.components.list","material.components.menuBar","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.showHide","material.components.select","material.components.sidenav","material.components.slider","material.components.sticky","material.components.subheader","material.components.swipe","material.components.switch","material.components.tabs","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.virtualRepeat","material.components.whiteframe"]); 14 })(); 15 (function(){ 16 "use strict"; 17 18 /** 19 * Initialization function that validates environment 20 * requirements. 21 */ 22 angular 23 .module('material.core', [ 24 'ngAnimate', 25 'material.core.animate', 26 'material.core.layout', 27 'material.core.gestures', 28 'material.core.theming' 29 ]) 30 .config(MdCoreConfigure) 31 .run(DetectNgTouch); 32 33 34 /** 35 * Detect if the ng-Touch module is also being used. 36 * Warn if detected. 37 * @ngInject 38 */ 39 function DetectNgTouch($log, $injector) { 40 if ( $injector.has('$swipe') ) { 41 var msg = "" + 42 "You are using the ngTouch module. \n" + 43 "Angular Material already has mobile click, tap, and swipe support... \n" + 44 "ngTouch is not supported with Angular Material!"; 45 $log.warn(msg); 46 } 47 } 48 DetectNgTouch.$inject = ["$log", "$injector"]; 49 50 /** 51 * @ngInject 52 */ 53 function MdCoreConfigure($provide, $mdThemingProvider) { 54 55 $provide.decorator('$$rAF', ["$delegate", rAFDecorator]); 56 57 $mdThemingProvider.theme('default') 58 .primaryPalette('indigo') 59 .accentPalette('pink') 60 .warnPalette('deep-orange') 61 .backgroundPalette('grey'); 62 } 63 MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"]; 64 65 /** 66 * @ngInject 67 */ 68 function rAFDecorator($delegate) { 69 /** 70 * Use this to throttle events that come in often. 71 * The throttled function will always use the *last* invocation before the 72 * coming frame. 73 * 74 * For example, window resize events that fire many times a second: 75 * If we set to use an raf-throttled callback on window resize, then 76 * our callback will only be fired once per frame, with the last resize 77 * event that happened before that frame. 78 * 79 * @param {function} callback function to debounce 80 */ 81 $delegate.throttle = function(cb) { 82 var queuedArgs, alreadyQueued, queueCb, context; 83 return function debounced() { 84 queuedArgs = arguments; 85 context = this; 86 queueCb = cb; 87 if (!alreadyQueued) { 88 alreadyQueued = true; 89 $delegate(function() { 90 queueCb.apply(context, Array.prototype.slice.call(queuedArgs)); 91 alreadyQueued = false; 92 }); 93 } 94 }; 95 }; 96 return $delegate; 97 } 98 rAFDecorator.$inject = ["$delegate"]; 99 100 })(); 101 (function(){ 102 "use strict"; 103 104 angular.module('material.core') 105 .directive('mdAutofocus', MdAutofocusDirective) 106 107 // Support the deprecated md-auto-focus and md-sidenav-focus as well 108 .directive('mdAutoFocus', MdAutofocusDirective) 109 .directive('mdSidenavFocus', MdAutofocusDirective); 110 111 /** 112 * @ngdoc directive 113 * @name mdAutofocus 114 * @module material.core.util 115 * 116 * @description 117 * 118 * `[md-autofocus]` provides an optional way to identify the focused element when a `$mdDialog`, 119 * `$mdBottomSheet`, or `$mdSidenav` opens or upon page load for input-like elements. 120 * 121 * When one of these opens, it will find the first nested element with the `[md-autofocus]` 122 * attribute directive and optional expression. An expression may be specified as the directive 123 * value to enable conditional activation of the autofocus. 124 * 125 * @usage 126 * 127 * ### Dialog 128 * <hljs lang="html"> 129 * <md-dialog> 130 * <form> 131 * <md-input-container> 132 * <label for="testInput">Label</label> 133 * <input id="testInput" type="text" md-autofocus> 134 * </md-input-container> 135 * </form> 136 * </md-dialog> 137 * </hljs> 138 * 139 * ### Bottomsheet 140 * <hljs lang="html"> 141 * <md-bottom-sheet class="md-list md-has-header"> 142 * <md-subheader>Comment Actions</md-subheader> 143 * <md-list> 144 * <md-list-item ng-repeat="item in items"> 145 * 146 * <md-button md-autofocus="$index == 2"> 147 * <md-icon md-svg-src="{{item.icon}}"></md-icon> 148 * <span class="md-inline-list-icon-label">{{ item.name }}</span> 149 * </md-button> 150 * 151 * </md-list-item> 152 * </md-list> 153 * </md-bottom-sheet> 154 * </hljs> 155 * 156 * ### Autocomplete 157 * <hljs lang="html"> 158 * <md-autocomplete 159 * md-autofocus 160 * md-selected-item="selectedItem" 161 * md-search-text="searchText" 162 * md-items="item in getMatches(searchText)" 163 * md-item-text="item.display"> 164 * <span md-highlight-text="searchText">{{item.display}}</span> 165 * </md-autocomplete> 166 * </hljs> 167 * 168 * ### Sidenav 169 * <hljs lang="html"> 170 * <div layout="row" ng-controller="MyController"> 171 * <md-sidenav md-component-id="left" class="md-sidenav-left"> 172 * Left Nav! 173 * </md-sidenav> 174 * 175 * <md-content> 176 * Center Content 177 * <md-button ng-click="openLeftMenu()"> 178 * Open Left Menu 179 * </md-button> 180 * </md-content> 181 * 182 * <md-sidenav md-component-id="right" 183 * md-is-locked-open="$mdMedia('min-width: 333px')" 184 * class="md-sidenav-right"> 185 * <form> 186 * <md-input-container> 187 * <label for="testInput">Test input</label> 188 * <input id="testInput" type="text" 189 * ng-model="data" md-autofocus> 190 * </md-input-container> 191 * </form> 192 * </md-sidenav> 193 * </div> 194 * </hljs> 195 **/ 196 function MdAutofocusDirective() { 197 return { 198 restrict: 'A', 199 200 link: postLink 201 } 202 } 203 204 function postLink(scope, element, attrs) { 205 var attr = attrs.mdAutoFocus || attrs.mdAutofocus || attrs.mdSidenavFocus; 206 207 // Setup a watcher on the proper attribute to update a class we can check for in $mdUtil 208 scope.$watch(attr, function(canAutofocus) { 209 element.toggleClass('_md-autofocus', canAutofocus); 210 }); 211 } 212 213 })(); 214 (function(){ 215 "use strict"; 216 217 angular.module('material.core') 218 .factory('$mdConstant', MdConstantFactory); 219 220 /** 221 * Factory function that creates the grab-bag $mdConstant service. 222 * @ngInject 223 */ 224 function MdConstantFactory($sniffer) { 225 226 var webkit = /webkit/i.test($sniffer.vendorPrefix); 227 function vendorProperty(name) { 228 return webkit ? ('webkit' + name.charAt(0).toUpperCase() + name.substring(1)) : name; 229 } 230 231 return { 232 KEY_CODE: { 233 COMMA: 188, 234 SEMICOLON : 186, 235 ENTER: 13, 236 ESCAPE: 27, 237 SPACE: 32, 238 PAGE_UP: 33, 239 PAGE_DOWN: 34, 240 END: 35, 241 HOME: 36, 242 LEFT_ARROW : 37, 243 UP_ARROW : 38, 244 RIGHT_ARROW : 39, 245 DOWN_ARROW : 40, 246 TAB : 9, 247 BACKSPACE: 8, 248 DELETE: 46 249 }, 250 CSS: { 251 /* Constants */ 252 TRANSITIONEND: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''), 253 ANIMATIONEND: 'animationend' + (webkit ? ' webkitAnimationEnd' : ''), 254 255 TRANSFORM: vendorProperty('transform'), 256 TRANSFORM_ORIGIN: vendorProperty('transformOrigin'), 257 TRANSITION: vendorProperty('transition'), 258 TRANSITION_DURATION: vendorProperty('transitionDuration'), 259 ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'), 260 ANIMATION_DURATION: vendorProperty('animationDuration'), 261 ANIMATION_NAME: vendorProperty('animationName'), 262 ANIMATION_TIMING: vendorProperty('animationTimingFunction'), 263 ANIMATION_DIRECTION: vendorProperty('animationDirection') 264 }, 265 /** 266 * As defined in core/style/variables.scss 267 * 268 * $layout-breakpoint-xs: 600px !default; 269 * $layout-breakpoint-sm: 960px !default; 270 * $layout-breakpoint-md: 1280px !default; 271 * $layout-breakpoint-lg: 1920px !default; 272 * 273 */ 274 MEDIA: { 275 'xs' : '(max-width: 599px)' , 276 'gt-xs' : '(min-width: 600px)' , 277 'sm' : '(min-width: 600px) and (max-width: 959px)' , 278 'gt-sm' : '(min-width: 960px)' , 279 'md' : '(min-width: 960px) and (max-width: 1279px)' , 280 'gt-md' : '(min-width: 1280px)' , 281 'lg' : '(min-width: 1280px) and (max-width: 1919px)', 282 'gt-lg' : '(min-width: 1920px)' , 283 'xl' : '(min-width: 1920px)' , 284 'print' : 'print' 285 }, 286 MEDIA_PRIORITY: [ 287 'xl', 288 'gt-lg', 289 'lg', 290 'gt-md', 291 'md', 292 'gt-sm', 293 'sm', 294 'gt-xs', 295 'xs', 296 'print' 297 ] 298 }; 299 } 300 MdConstantFactory.$inject = ["$sniffer"]; 301 302 })(); 303 (function(){ 304 "use strict"; 305 306 angular 307 .module('material.core') 308 .config( ["$provide", function($provide){ 309 $provide.decorator('$mdUtil', ['$delegate', function ($delegate){ 310 /** 311 * Inject the iterator facade to easily support iteration and accessors 312 * @see iterator below 313 */ 314 $delegate.iterator = MdIterator; 315 316 return $delegate; 317 } 318 ]); 319 }]); 320 321 /** 322 * iterator is a list facade to easily support iteration and accessors 323 * 324 * @param items Array list which this iterator will enumerate 325 * @param reloop Boolean enables iterator to consider the list as an endless reloop 326 */ 327 function MdIterator(items, reloop) { 328 var trueFn = function() { return true; }; 329 330 if (items && !angular.isArray(items)) { 331 items = Array.prototype.slice.call(items); 332 } 333 334 reloop = !!reloop; 335 var _items = items || [ ]; 336 337 // Published API 338 return { 339 items: getItems, 340 count: count, 341 342 inRange: inRange, 343 contains: contains, 344 indexOf: indexOf, 345 itemAt: itemAt, 346 347 findBy: findBy, 348 349 add: add, 350 remove: remove, 351 352 first: first, 353 last: last, 354 next: angular.bind(null, findSubsequentItem, false), 355 previous: angular.bind(null, findSubsequentItem, true), 356 357 hasPrevious: hasPrevious, 358 hasNext: hasNext 359 360 }; 361 362 /** 363 * Publish copy of the enumerable set 364 * @returns {Array|*} 365 */ 366 function getItems() { 367 return [].concat(_items); 368 } 369 370 /** 371 * Determine length of the list 372 * @returns {Array.length|*|number} 373 */ 374 function count() { 375 return _items.length; 376 } 377 378 /** 379 * Is the index specified valid 380 * @param index 381 * @returns {Array.length|*|number|boolean} 382 */ 383 function inRange(index) { 384 return _items.length && ( index > -1 ) && (index < _items.length ); 385 } 386 387 /** 388 * Can the iterator proceed to the next item in the list; relative to 389 * the specified item. 390 * 391 * @param item 392 * @returns {Array.length|*|number|boolean} 393 */ 394 function hasNext(item) { 395 return item ? inRange(indexOf(item) + 1) : false; 396 } 397 398 /** 399 * Can the iterator proceed to the previous item in the list; relative to 400 * the specified item. 401 * 402 * @param item 403 * @returns {Array.length|*|number|boolean} 404 */ 405 function hasPrevious(item) { 406 return item ? inRange(indexOf(item) - 1) : false; 407 } 408 409 /** 410 * Get item at specified index/position 411 * @param index 412 * @returns {*} 413 */ 414 function itemAt(index) { 415 return inRange(index) ? _items[index] : null; 416 } 417 418 /** 419 * Find all elements matching the key/value pair 420 * otherwise return null 421 * 422 * @param val 423 * @param key 424 * 425 * @return array 426 */ 427 function findBy(key, val) { 428 return _items.filter(function(item) { 429 return item[key] === val; 430 }); 431 } 432 433 /** 434 * Add item to list 435 * @param item 436 * @param index 437 * @returns {*} 438 */ 439 function add(item, index) { 440 if ( !item ) return -1; 441 442 if (!angular.isNumber(index)) { 443 index = _items.length; 444 } 445 446 _items.splice(index, 0, item); 447 448 return indexOf(item); 449 } 450 451 /** 452 * Remove item from list... 453 * @param item 454 */ 455 function remove(item) { 456 if ( contains(item) ){ 457 _items.splice(indexOf(item), 1); 458 } 459 } 460 461 /** 462 * Get the zero-based index of the target item 463 * @param item 464 * @returns {*} 465 */ 466 function indexOf(item) { 467 return _items.indexOf(item); 468 } 469 470 /** 471 * Boolean existence check 472 * @param item 473 * @returns {boolean} 474 */ 475 function contains(item) { 476 return item && (indexOf(item) > -1); 477 } 478 479 /** 480 * Return first item in the list 481 * @returns {*} 482 */ 483 function first() { 484 return _items.length ? _items[0] : null; 485 } 486 487 /** 488 * Return last item in the list... 489 * @returns {*} 490 */ 491 function last() { 492 return _items.length ? _items[_items.length - 1] : null; 493 } 494 495 /** 496 * Find the next item. If reloop is true and at the end of the list, it will go back to the 497 * first item. If given, the `validate` callback will be used to determine whether the next item 498 * is valid. If not valid, it will try to find the next item again. 499 * 500 * @param {boolean} backwards Specifies the direction of searching (forwards/backwards) 501 * @param {*} item The item whose subsequent item we are looking for 502 * @param {Function=} validate The `validate` function 503 * @param {integer=} limit The recursion limit 504 * 505 * @returns {*} The subsequent item or null 506 */ 507 function findSubsequentItem(backwards, item, validate, limit) { 508 validate = validate || trueFn; 509 510 var curIndex = indexOf(item); 511 while (true) { 512 if (!inRange(curIndex)) return null; 513 514 var nextIndex = curIndex + (backwards ? -1 : 1); 515 var foundItem = null; 516 if (inRange(nextIndex)) { 517 foundItem = _items[nextIndex]; 518 } else if (reloop) { 519 foundItem = backwards ? last() : first(); 520 nextIndex = indexOf(foundItem); 521 } 522 523 if ((foundItem === null) || (nextIndex === limit)) return null; 524 if (validate(foundItem)) return foundItem; 525 526 if (angular.isUndefined(limit)) limit = nextIndex; 527 528 curIndex = nextIndex; 529 } 530 } 531 } 532 533 534 })(); 535 (function(){ 536 "use strict"; 537 538 angular.module('material.core') 539 .factory('$mdMedia', mdMediaFactory); 540 541 /** 542 * @ngdoc service 543 * @name $mdMedia 544 * @module material.core 545 * 546 * @description 547 * `$mdMedia` is used to evaluate whether a given media query is true or false given the 548 * current device's screen / window size. The media query will be re-evaluated on resize, allowing 549 * you to register a watch. 550 * 551 * `$mdMedia` also has pre-programmed support for media queries that match the layout breakpoints: 552 * 553 * <table class="md-api-table"> 554 * <thead> 555 * <tr> 556 * <th>Breakpoint</th> 557 * <th>mediaQuery</th> 558 * </tr> 559 * </thead> 560 * <tbody> 561 * <tr> 562 * <td>xs</td> 563 * <td>(max-width: 599px)</td> 564 * </tr> 565 * <tr> 566 * <td>gt-xs</td> 567 * <td>(min-width: 600px)</td> 568 * </tr> 569 * <tr> 570 * <td>sm</td> 571 * <td>(min-width: 600px) and (max-width: 959px)</td> 572 * </tr> 573 * <tr> 574 * <td>gt-sm</td> 575 * <td>(min-width: 960px)</td> 576 * </tr> 577 * <tr> 578 * <td>md</td> 579 * <td>(min-width: 960px) and (max-width: 1279px)</td> 580 * </tr> 581 * <tr> 582 * <td>gt-md</td> 583 * <td>(min-width: 1280px)</td> 584 * </tr> 585 * <tr> 586 * <td>lg</td> 587 * <td>(min-width: 1280px) and (max-width: 1919px)</td> 588 * </tr> 589 * <tr> 590 * <td>gt-lg</td> 591 * <td>(min-width: 1920px)</td> 592 * </tr> 593 * <tr> 594 * <td>xl</td> 595 * <td>(min-width: 1920px)</td> 596 * </tr> 597 * <tr> 598 * <td>print</td> 599 * <td>print</td> 600 * </tr> 601 * </tbody> 602 * </table> 603 * 604 * See Material Design's <a href="https://www.google.com/design/spec/layout/adaptive-ui.html">Layout - Adaptive UI</a> for more details. 605 * 606 * <a href="https://www.google.com/design/spec/layout/adaptive-ui.html"> 607 * <img src="https://material-design.storage.googleapis.com/publish/material_v_4/material_ext_publish/0B8olV15J7abPSGFxemFiQVRtb1k/layout_adaptive_breakpoints_01.png" width="100%" height="100%"></img> 608 * </a> 609 * 610 * @returns {boolean} a boolean representing whether or not the given media query is true or false. 611 * 612 * @usage 613 * <hljs lang="js"> 614 * app.controller('MyController', function($mdMedia, $scope) { 615 * $scope.$watch(function() { return $mdMedia('lg'); }, function(big) { 616 * $scope.bigScreen = big; 617 * }); 618 * 619 * $scope.screenIsSmall = $mdMedia('sm'); 620 * $scope.customQuery = $mdMedia('(min-width: 1234px)'); 621 * $scope.anotherCustom = $mdMedia('max-width: 300px'); 622 * }); 623 * </hljs> 624 * @ngInject 625 */ 626 627 function mdMediaFactory($mdConstant, $rootScope, $window) { 628 var queries = {}; 629 var mqls = {}; 630 var results = {}; 631 var normalizeCache = {}; 632 633 $mdMedia.getResponsiveAttribute = getResponsiveAttribute; 634 $mdMedia.getQuery = getQuery; 635 $mdMedia.watchResponsiveAttributes = watchResponsiveAttributes; 636 637 return $mdMedia; 638 639 function $mdMedia(query) { 640 var validated = queries[query]; 641 if (angular.isUndefined(validated)) { 642 validated = queries[query] = validate(query); 643 } 644 645 var result = results[validated]; 646 if (angular.isUndefined(result)) { 647 result = add(validated); 648 } 649 650 return result; 651 } 652 653 function validate(query) { 654 return $mdConstant.MEDIA[query] || 655 ((query.charAt(0) !== '(') ? ('(' + query + ')') : query); 656 } 657 658 function add(query) { 659 var result = mqls[query]; 660 if ( !result ) { 661 result = mqls[query] = $window.matchMedia(query); 662 } 663 664 result.addListener(onQueryChange); 665 return (results[result.media] = !!result.matches); 666 } 667 668 function onQueryChange(query) { 669 $rootScope.$evalAsync(function() { 670 results[query.media] = !!query.matches; 671 }); 672 } 673 674 function getQuery(name) { 675 return mqls[name]; 676 } 677 678 function getResponsiveAttribute(attrs, attrName) { 679 for (var i = 0; i < $mdConstant.MEDIA_PRIORITY.length; i++) { 680 var mediaName = $mdConstant.MEDIA_PRIORITY[i]; 681 if (!mqls[queries[mediaName]].matches) { 682 continue; 683 } 684 685 var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); 686 if (attrs[normalizedName]) { 687 return attrs[normalizedName]; 688 } 689 } 690 691 // fallback on unprefixed 692 return attrs[getNormalizedName(attrs, attrName)]; 693 } 694 695 function watchResponsiveAttributes(attrNames, attrs, watchFn) { 696 var unwatchFns = []; 697 attrNames.forEach(function(attrName) { 698 var normalizedName = getNormalizedName(attrs, attrName); 699 if (angular.isDefined(attrs[normalizedName])) { 700 unwatchFns.push( 701 attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null))); 702 } 703 704 for (var mediaName in $mdConstant.MEDIA) { 705 normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); 706 if (angular.isDefined(attrs[normalizedName])) { 707 unwatchFns.push( 708 attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName))); 709 } 710 } 711 }); 712 713 return function unwatch() { 714 unwatchFns.forEach(function(fn) { fn(); }) 715 }; 716 } 717 718 // Improves performance dramatically 719 function getNormalizedName(attrs, attrName) { 720 return normalizeCache[attrName] || 721 (normalizeCache[attrName] = attrs.$normalize(attrName)); 722 } 723 } 724 mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"]; 725 726 })(); 727 (function(){ 728 "use strict"; 729 730 /* 731 * This var has to be outside the angular factory, otherwise when 732 * there are multiple material apps on the same page, each app 733 * will create its own instance of this array and the app's IDs 734 * will not be unique. 735 */ 736 var nextUniqueId = 0; 737 738 /** 739 * @ngdoc module 740 * @name material.core.util 741 * @description 742 * Util 743 */ 744 angular 745 .module('material.core') 746 .factory('$mdUtil', UtilFactory); 747 748 /** 749 * @ngInject 750 */ 751 function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window) { 752 // Setup some core variables for the processTemplate method 753 var startSymbol = $interpolate.startSymbol(), 754 endSymbol = $interpolate.endSymbol(), 755 usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}')); 756 757 /** 758 * Checks if the target element has the requested style by key 759 * @param {DOMElement|JQLite} target Target element 760 * @param {string} key Style key 761 * @param {string=} expectedVal Optional expected value 762 * @returns {boolean} Whether the target element has the style or not 763 */ 764 var hasComputedStyle = function (target, key, expectedVal) { 765 var hasValue = false; 766 767 if ( target && target.length ) { 768 var computedStyles = $window.getComputedStyle(target[0]); 769 hasValue = angular.isDefined(computedStyles[key]) && (expectedVal ? computedStyles[key] == expectedVal : true); 770 } 771 772 return hasValue; 773 }; 774 775 var $mdUtil = { 776 dom: {}, 777 now: window.performance ? 778 angular.bind(window.performance, window.performance.now) : Date.now || function() { 779 return new Date().getTime(); 780 }, 781 782 clientRect: function(element, offsetParent, isOffsetRect) { 783 var node = getNode(element); 784 offsetParent = getNode(offsetParent || node.offsetParent || document.body); 785 var nodeRect = node.getBoundingClientRect(); 786 787 // The user can ask for an offsetRect: a rect relative to the offsetParent, 788 // or a clientRect: a rect relative to the page 789 var offsetRect = isOffsetRect ? 790 offsetParent.getBoundingClientRect() : 791 {left: 0, top: 0, width: 0, height: 0}; 792 return { 793 left: nodeRect.left - offsetRect.left, 794 top: nodeRect.top - offsetRect.top, 795 width: nodeRect.width, 796 height: nodeRect.height 797 }; 798 }, 799 offsetRect: function(element, offsetParent) { 800 return $mdUtil.clientRect(element, offsetParent, true); 801 }, 802 803 // Annoying method to copy nodes to an array, thanks to IE 804 nodesToArray: function(nodes) { 805 nodes = nodes || []; 806 807 var results = []; 808 for (var i = 0; i < nodes.length; ++i) { 809 results.push(nodes.item(i)); 810 } 811 return results; 812 }, 813 814 /** 815 * Calculate the positive scroll offset 816 * TODO: Check with pinch-zoom in IE/Chrome; 817 * https://code.google.com/p/chromium/issues/detail?id=496285 818 */ 819 scrollTop: function(element) { 820 element = angular.element(element || $document[0].body); 821 822 var body = (element[0] == $document[0].body) ? $document[0].body : undefined; 823 var scrollTop = body ? body.scrollTop + body.parentElement.scrollTop : 0; 824 825 // Calculate the positive scroll offset 826 return scrollTop || Math.abs(element[0].getBoundingClientRect().top); 827 }, 828 829 /** 830 * Finds the proper focus target by searching the DOM. 831 * 832 * @param containerEl 833 * @param attributeVal 834 * @returns {*} 835 */ 836 findFocusTarget: function(containerEl, attributeVal) { 837 var AUTO_FOCUS = '[md-autofocus]'; 838 var elToFocus; 839 840 elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS); 841 842 if ( !elToFocus && attributeVal != AUTO_FOCUS) { 843 // Scan for deprecated attribute 844 elToFocus = scanForFocusable(containerEl, '[md-auto-focus]'); 845 846 if ( !elToFocus ) { 847 // Scan for fallback to 'universal' API 848 elToFocus = scanForFocusable(containerEl, AUTO_FOCUS); 849 } 850 } 851 852 return elToFocus; 853 854 /** 855 * Can target and nested children for specified Selector (attribute) 856 * whose value may be an expression that evaluates to True/False. 857 */ 858 function scanForFocusable(target, selector) { 859 var elFound, items = target[0].querySelectorAll(selector); 860 861 // Find the last child element with the focus attribute 862 if ( items && items.length ){ 863 items.length && angular.forEach(items, function(it) { 864 it = angular.element(it); 865 866 // Check the element for the _md-autofocus class to ensure any associated expression 867 // evaluated to true. 868 var isFocusable = it.hasClass('_md-autofocus'); 869 if (isFocusable) elFound = it; 870 }); 871 } 872 return elFound; 873 } 874 }, 875 876 // Disables scroll around the passed element. 877 disableScrollAround: function(element, parent) { 878 $mdUtil.disableScrollAround._count = $mdUtil.disableScrollAround._count || 0; 879 ++$mdUtil.disableScrollAround._count; 880 if ($mdUtil.disableScrollAround._enableScrolling) return $mdUtil.disableScrollAround._enableScrolling; 881 element = angular.element(element); 882 var body = $document[0].body, 883 restoreBody = disableBodyScroll(), 884 restoreElement = disableElementScroll(parent); 885 886 return $mdUtil.disableScrollAround._enableScrolling = function() { 887 if (!--$mdUtil.disableScrollAround._count) { 888 restoreBody(); 889 restoreElement(); 890 delete $mdUtil.disableScrollAround._enableScrolling; 891 } 892 }; 893 894 // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events 895 function disableElementScroll(element) { 896 element = angular.element(element || body)[0]; 897 var zIndex = 50; 898 var scrollMask = angular.element( 899 '<div class="md-scroll-mask">' + 900 ' <div class="md-scroll-mask-bar"></div>' + 901 '</div>').css('z-index', zIndex); 902 element.appendChild(scrollMask[0]); 903 904 scrollMask.on('wheel', preventDefault); 905 scrollMask.on('touchmove', preventDefault); 906 907 return function restoreScroll() { 908 scrollMask.off('wheel'); 909 scrollMask.off('touchmove'); 910 scrollMask[0].parentNode.removeChild(scrollMask[0]); 911 delete $mdUtil.disableScrollAround._enableScrolling; 912 }; 913 914 function preventDefault(e) { 915 e.preventDefault(); 916 } 917 } 918 919 // Converts the body to a position fixed block and translate it to the proper scroll 920 // position 921 function disableBodyScroll() { 922 var htmlNode = body.parentNode; 923 var restoreHtmlStyle = htmlNode.style.cssText || ''; 924 var restoreBodyStyle = body.style.cssText || ''; 925 var scrollOffset = $mdUtil.scrollTop(body); 926 var clientWidth = body.clientWidth; 927 928 if (body.scrollHeight > body.clientHeight + 1) { 929 applyStyles(body, { 930 position: 'fixed', 931 width: '100%', 932 top: -scrollOffset + 'px' 933 }); 934 935 applyStyles(htmlNode, { 936 overflowY: 'scroll' 937 }); 938 } 939 940 if (body.clientWidth < clientWidth) applyStyles(body, {overflow: 'hidden'}); 941 942 return function restoreScroll() { 943 body.style.cssText = restoreBodyStyle; 944 htmlNode.style.cssText = restoreHtmlStyle; 945 body.scrollTop = scrollOffset; 946 htmlNode.scrollTop = scrollOffset; 947 }; 948 } 949 950 function applyStyles(el, styles) { 951 for (var key in styles) { 952 el.style[key] = styles[key]; 953 } 954 } 955 }, 956 enableScrolling: function() { 957 var method = this.disableScrollAround._enableScrolling; 958 method && method(); 959 }, 960 floatingScrollbars: function() { 961 if (this.floatingScrollbars.cached === undefined) { 962 var tempNode = angular.element('<div><div></div></div>').css({ 963 width: '100%', 964 'z-index': -1, 965 position: 'absolute', 966 height: '35px', 967 'overflow-y': 'scroll' 968 }); 969 tempNode.children().css('height', '60px'); 970 971 $document[0].body.appendChild(tempNode[0]); 972 this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth); 973 tempNode.remove(); 974 } 975 return this.floatingScrollbars.cached; 976 }, 977 978 // Mobile safari only allows you to set focus in click event listeners... 979 forceFocus: function(element) { 980 var node = element[0] || element; 981 982 document.addEventListener('click', function focusOnClick(ev) { 983 if (ev.target === node && ev.$focus) { 984 node.focus(); 985 ev.stopImmediatePropagation(); 986 ev.preventDefault(); 987 node.removeEventListener('click', focusOnClick); 988 } 989 }, true); 990 991 var newEvent = document.createEvent('MouseEvents'); 992 newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0, 993 false, false, false, false, 0, null); 994 newEvent.$material = true; 995 newEvent.$focus = true; 996 node.dispatchEvent(newEvent); 997 }, 998 999 /** 1000 * facade to build md-backdrop element with desired styles 1001 * NOTE: Use $compile to trigger backdrop postLink function 1002 */ 1003 createBackdrop: function(scope, addClass) { 1004 return $compile($mdUtil.supplant('<md-backdrop class="{0}">', [addClass]))(scope); 1005 }, 1006 1007 /** 1008 * supplant() method from Crockford's `Remedial Javascript` 1009 * Equivalent to use of $interpolate; without dependency on 1010 * interpolation symbols and scope. Note: the '{<token>}' can 1011 * be property names, property chains, or array indices. 1012 */ 1013 supplant: function(template, values, pattern) { 1014 pattern = pattern || /\{([^\{\}]*)\}/g; 1015 return template.replace(pattern, function(a, b) { 1016 var p = b.split('.'), 1017 r = values; 1018 try { 1019 for (var s in p) { 1020 if (p.hasOwnProperty(s) ) { 1021 r = r[p[s]]; 1022 } 1023 } 1024 } catch (e) { 1025 r = a; 1026 } 1027 return (typeof r === 'string' || typeof r === 'number') ? r : a; 1028 }); 1029 }, 1030 1031 fakeNgModel: function() { 1032 return { 1033 $fake: true, 1034 $setTouched: angular.noop, 1035 $setViewValue: function(value) { 1036 this.$viewValue = value; 1037 this.$render(value); 1038 this.$viewChangeListeners.forEach(function(cb) { 1039 cb(); 1040 }); 1041 }, 1042 $isEmpty: function(value) { 1043 return ('' + value).length === 0; 1044 }, 1045 $parsers: [], 1046 $formatters: [], 1047 $viewChangeListeners: [], 1048 $render: angular.noop 1049 }; 1050 }, 1051 1052 // Returns a function, that, as long as it continues to be invoked, will not 1053 // be triggered. The function will be called after it stops being called for 1054 // N milliseconds. 1055 // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs 1056 // @param invokeApply should the $timeout trigger $digest() dirty checking 1057 debounce: function(func, wait, scope, invokeApply) { 1058 var timer; 1059 1060 return function debounced() { 1061 var context = scope, 1062 args = Array.prototype.slice.call(arguments); 1063 1064 $timeout.cancel(timer); 1065 timer = $timeout(function() { 1066 1067 timer = undefined; 1068 func.apply(context, args); 1069 1070 }, wait || 10, invokeApply); 1071 }; 1072 }, 1073 1074 // Returns a function that can only be triggered every `delay` milliseconds. 1075 // In other words, the function will not be called unless it has been more 1076 // than `delay` milliseconds since the last call. 1077 throttle: function throttle(func, delay) { 1078 var recent; 1079 return function throttled() { 1080 var context = this; 1081 var args = arguments; 1082 var now = $mdUtil.now(); 1083 1084 if (!recent || (now - recent > delay)) { 1085 func.apply(context, args); 1086 recent = now; 1087 } 1088 }; 1089 }, 1090 1091 /** 1092 * Measures the number of milliseconds taken to run the provided callback 1093 * function. Uses a high-precision timer if available. 1094 */ 1095 time: function time(cb) { 1096 var start = $mdUtil.now(); 1097 cb(); 1098 return $mdUtil.now() - start; 1099 }, 1100 1101 /** 1102 * Create an implicit getter that caches its `getter()` 1103 * lookup value 1104 */ 1105 valueOnUse : function (scope, key, getter) { 1106 var value = null, args = Array.prototype.slice.call(arguments); 1107 var params = (args.length > 3) ? args.slice(3) : [ ]; 1108 1109 Object.defineProperty(scope, key, { 1110 get: function () { 1111 if (value === null) value = getter.apply(scope, params); 1112 return value; 1113 } 1114 }); 1115 }, 1116 1117 /** 1118 * Get a unique ID. 1119 * 1120 * @returns {string} an unique numeric string 1121 */ 1122 nextUid: function() { 1123 return '' + nextUniqueId++; 1124 }, 1125 1126 // Stop watchers and events from firing on a scope without destroying it, 1127 // by disconnecting it from its parent and its siblings' linked lists. 1128 disconnectScope: function disconnectScope(scope) { 1129 if (!scope) return; 1130 1131 // we can't destroy the root scope or a scope that has been already destroyed 1132 if (scope.$root === scope) return; 1133 if (scope.$$destroyed) return; 1134 1135 var parent = scope.$parent; 1136 scope.$$disconnected = true; 1137 1138 // See Scope.$destroy 1139 if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling; 1140 if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling; 1141 if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling; 1142 if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling; 1143 1144 scope.$$nextSibling = scope.$$prevSibling = null; 1145 1146 }, 1147 1148 // Undo the effects of disconnectScope above. 1149 reconnectScope: function reconnectScope(scope) { 1150 if (!scope) return; 1151 1152 // we can't disconnect the root node or scope already disconnected 1153 if (scope.$root === scope) return; 1154 if (!scope.$$disconnected) return; 1155 1156 var child = scope; 1157 1158 var parent = child.$parent; 1159 child.$$disconnected = false; 1160 // See Scope.$new for this logic... 1161 child.$$prevSibling = parent.$$childTail; 1162 if (parent.$$childHead) { 1163 parent.$$childTail.$$nextSibling = child; 1164 parent.$$childTail = child; 1165 } else { 1166 parent.$$childHead = parent.$$childTail = child; 1167 } 1168 }, 1169 1170 /* 1171 * getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName 1172 * 1173 * @param el Element to start walking the DOM from 1174 * @param tagName Tag name to find closest to el, such as 'form' 1175 * @param onlyParent Only start checking from the parent element, not `el`. 1176 */ 1177 getClosest: function getClosest(el, tagName, onlyParent) { 1178 if (el instanceof angular.element) el = el[0]; 1179 tagName = tagName.toUpperCase(); 1180 if (onlyParent) el = el.parentNode; 1181 if (!el) return null; 1182 do { 1183 if (el.nodeName === tagName) { 1184 return el; 1185 } 1186 } while (el = el.parentNode); 1187 return null; 1188 }, 1189 1190 /** 1191 * Build polyfill for the Node.contains feature (if needed) 1192 */ 1193 elementContains: function(node, child) { 1194 var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains); 1195 var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) { 1196 // compares the positions of two nodes and returns a bitmask 1197 return (node === child) || !!(this.compareDocumentPosition(arg) & 16) 1198 }); 1199 1200 return findFn(child); 1201 }, 1202 1203 /** 1204 * Functional equivalent for $element.filter(‘md-bottom-sheet’) 1205 * useful with interimElements where the element and its container are important... 1206 * 1207 * @param {[]} elements to scan 1208 * @param {string} name of node to find (e.g. 'md-dialog') 1209 * @param {boolean=} optional flag to allow deep scans; defaults to 'false'. 1210 * @param {boolean=} optional flag to enable log warnings; defaults to false 1211 */ 1212 extractElementByName: function(element, nodeName, scanDeep, warnNotFound) { 1213 var found = scanTree(element); 1214 if (!found && !!warnNotFound) { 1215 $log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) ); 1216 } 1217 1218 return angular.element(found || element); 1219 1220 /** 1221 * Breadth-First tree scan for element with matching `nodeName` 1222 */ 1223 function scanTree(element) { 1224 return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null); 1225 } 1226 1227 /** 1228 * Case-insensitive scan of current elements only (do not descend). 1229 */ 1230 function scanLevel(element) { 1231 if ( element ) { 1232 for (var i = 0, len = element.length; i < len; i++) { 1233 if (element[i].nodeName.toLowerCase() === nodeName) { 1234 return element[i]; 1235 } 1236 } 1237 } 1238 return null; 1239 } 1240 1241 /** 1242 * Scan children of specified node 1243 */ 1244 function scanChildren(element) { 1245 var found; 1246 if ( element ) { 1247 for (var i = 0, len = element.length; i < len; i++) { 1248 var target = element[i]; 1249 if ( !found ) { 1250 for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) { 1251 found = found || scanTree([target.childNodes[j]]); 1252 } 1253 } 1254 } 1255 } 1256 return found; 1257 } 1258 1259 }, 1260 1261 /** 1262 * Give optional properties with no value a boolean true if attr provided or false otherwise 1263 */ 1264 initOptionalProperties: function(scope, attr, defaults) { 1265 defaults = defaults || {}; 1266 angular.forEach(scope.$$isolateBindings, function(binding, key) { 1267 if (binding.optional && angular.isUndefined(scope[key])) { 1268 var attrIsDefined = angular.isDefined(attr[binding.attrName]); 1269 scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined; 1270 } 1271 }); 1272 }, 1273 1274 /** 1275 * Alternative to $timeout calls with 0 delay. 1276 * nextTick() coalesces all calls within a single frame 1277 * to minimize $digest thrashing 1278 * 1279 * @param callback 1280 * @param digest 1281 * @returns {*} 1282 */ 1283 nextTick: function(callback, digest, scope) { 1284 //-- grab function reference for storing state details 1285 var nextTick = $mdUtil.nextTick; 1286 var timeout = nextTick.timeout; 1287 var queue = nextTick.queue || []; 1288 1289 //-- add callback to the queue 1290 queue.push(callback); 1291 1292 //-- set default value for digest 1293 if (digest == null) digest = true; 1294 1295 //-- store updated digest/queue values 1296 nextTick.digest = nextTick.digest || digest; 1297 nextTick.queue = queue; 1298 1299 //-- either return existing timeout or create a new one 1300 return timeout || (nextTick.timeout = $timeout(processQueue, 0, false)); 1301 1302 /** 1303 * Grab a copy of the current queue 1304 * Clear the queue for future use 1305 * Process the existing queue 1306 * Trigger digest if necessary 1307 */ 1308 function processQueue() { 1309 var skip = scope && scope.$$destroyed; 1310 var queue = !skip ? nextTick.queue : []; 1311 var digest = !skip ? nextTick.digest : null; 1312 1313 nextTick.queue = []; 1314 nextTick.timeout = null; 1315 nextTick.digest = false; 1316 1317 queue.forEach(function(callback) { 1318 callback(); 1319 }); 1320 1321 if (digest) $rootScope.$digest(); 1322 } 1323 }, 1324 1325 /** 1326 * Processes a template and replaces the start/end symbols if the application has 1327 * overridden them. 1328 * 1329 * @param template The template to process whose start/end tags may be replaced. 1330 * @returns {*} 1331 */ 1332 processTemplate: function(template) { 1333 if (usesStandardSymbols) { 1334 return template; 1335 } else { 1336 if (!template || !angular.isString(template)) return template; 1337 return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); 1338 } 1339 }, 1340 1341 /** 1342 * Scan up dom hierarchy for enabled parent; 1343 */ 1344 getParentWithPointerEvents: function (element) { 1345 var parent = element.parent(); 1346 1347 // jqLite might return a non-null, but still empty, parent; so check for parent and length 1348 while (hasComputedStyle(parent, 'pointer-events', 'none')) { 1349 parent = parent.parent(); 1350 } 1351 1352 return parent; 1353 }, 1354 1355 getNearestContentElement: function (element) { 1356 var current = element.parent()[0]; 1357 // Look for the nearest parent md-content, stopping at the rootElement. 1358 while (current && current !== $rootElement[0] && current !== document.body && current.nodeName.toUpperCase() !== 'MD-CONTENT') { 1359 current = current.parentNode; 1360 } 1361 return current; 1362 }, 1363 1364 /** 1365 * Parses an attribute value, mostly a string. 1366 * By default checks for negated values and returns `false´ if present. 1367 * Negated values are: (native falsy) and negative strings like: 1368 * `false` or `0`. 1369 * @param value Attribute value which should be parsed. 1370 * @param negatedCheck When set to false, won't check for negated values. 1371 * @returns {boolean} 1372 */ 1373 parseAttributeBoolean: function(value, negatedCheck) { 1374 return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0'); 1375 }, 1376 1377 hasComputedStyle: hasComputedStyle 1378 }; 1379 1380 // Instantiate other namespace utility methods 1381 1382 $mdUtil.dom.animator = $$mdAnimate($mdUtil); 1383 1384 return $mdUtil; 1385 1386 function getNode(el) { 1387 return el[0] || el; 1388 } 1389 1390 } 1391 UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log", "$rootElement", "$window"]; 1392 1393 /* 1394 * Since removing jQuery from the demos, some code that uses `element.focus()` is broken. 1395 * We need to add `element.focus()`, because it's testable unlike `element[0].focus`. 1396 */ 1397 1398 angular.element.prototype.focus = angular.element.prototype.focus || function() { 1399 if (this.length) { 1400 this[0].focus(); 1401 } 1402 return this; 1403 }; 1404 angular.element.prototype.blur = angular.element.prototype.blur || function() { 1405 if (this.length) { 1406 this[0].blur(); 1407 } 1408 return this; 1409 }; 1410 1411 1412 })(); 1413 (function(){ 1414 "use strict"; 1415 1416 1417 angular.module('material.core') 1418 .service('$mdAria', AriaService); 1419 1420 /* 1421 * @ngInject 1422 */ 1423 function AriaService($$rAF, $log, $window, $interpolate) { 1424 1425 return { 1426 expect: expect, 1427 expectAsync: expectAsync, 1428 expectWithText: expectWithText 1429 }; 1430 1431 /** 1432 * Check if expected attribute has been specified on the target element or child 1433 * @param element 1434 * @param attrName 1435 * @param {optional} defaultValue What to set the attr to if no value is found 1436 */ 1437 function expect(element, attrName, defaultValue) { 1438 1439 var node = angular.element(element)[0] || element; 1440 1441 // if node exists and neither it nor its children have the attribute 1442 if (node && 1443 ((!node.hasAttribute(attrName) || 1444 node.getAttribute(attrName).length === 0) && 1445 !childHasAttribute(node, attrName))) { 1446 1447 defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : ''; 1448 if (defaultValue.length) { 1449 element.attr(attrName, defaultValue); 1450 } else { 1451 $log.warn('ARIA: Attribute "', attrName, '", required for accessibility, is missing on node:', node); 1452 } 1453 1454 } 1455 } 1456 1457 function expectAsync(element, attrName, defaultValueGetter) { 1458 // Problem: when retrieving the element's contents synchronously to find the label, 1459 // the text may not be defined yet in the case of a binding. 1460 // There is a higher chance that a binding will be defined if we wait one frame. 1461 $$rAF(function() { 1462 expect(element, attrName, defaultValueGetter()); 1463 }); 1464 } 1465 1466 function expectWithText(element, attrName) { 1467 var content = getText(element) || ""; 1468 var hasBinding = content.indexOf($interpolate.startSymbol())>-1; 1469 1470 if ( hasBinding ) { 1471 expectAsync(element, attrName, function() { 1472 return getText(element); 1473 }); 1474 } else { 1475 expect(element, attrName, content); 1476 } 1477 } 1478 1479 function getText(element) { 1480 return (element.text() || "").trim(); 1481 } 1482 1483 function childHasAttribute(node, attrName) { 1484 var hasChildren = node.hasChildNodes(), 1485 hasAttr = false; 1486 1487 function isHidden(el) { 1488 var style = el.currentStyle ? el.currentStyle : $window.getComputedStyle(el); 1489 return (style.display === 'none'); 1490 } 1491 1492 if(hasChildren) { 1493 var children = node.childNodes; 1494 for(var i=0; i<children.length; i++){ 1495 var child = children[i]; 1496 if(child.nodeType === 1 && child.hasAttribute(attrName)) { 1497 if(!isHidden(child)){ 1498 hasAttr = true; 1499 } 1500 } 1501 } 1502 } 1503 return hasAttr; 1504 } 1505 } 1506 AriaService.$inject = ["$$rAF", "$log", "$window", "$interpolate"]; 1507 1508 })(); 1509 (function(){ 1510 "use strict"; 1511 1512 angular 1513 .module('material.core') 1514 .service('$mdCompiler', mdCompilerService); 1515 1516 function mdCompilerService($q, $http, $injector, $compile, $controller, $templateCache) { 1517 /* jshint validthis: true */ 1518 1519 /* 1520 * @ngdoc service 1521 * @name $mdCompiler 1522 * @module material.core 1523 * @description 1524 * The $mdCompiler service is an abstraction of angular's compiler, that allows the developer 1525 * to easily compile an element with a templateUrl, controller, and locals. 1526 * 1527 * @usage 1528 * <hljs lang="js"> 1529 * $mdCompiler.compile({ 1530 * templateUrl: 'modal.html', 1531 * controller: 'ModalCtrl', 1532 * locals: { 1533 * modal: myModalInstance; 1534 * } 1535 * }).then(function(compileData) { 1536 * compileData.element; // modal.html's template in an element 1537 * compileData.link(myScope); //attach controller & scope to element 1538 * }); 1539 * </hljs> 1540 */ 1541 1542 /* 1543 * @ngdoc method 1544 * @name $mdCompiler#compile 1545 * @description A helper to compile an HTML template/templateUrl with a given controller, 1546 * locals, and scope. 1547 * @param {object} options An options object, with the following properties: 1548 * 1549 * - `controller` - `{(string=|function()=}` Controller fn that should be associated with 1550 * newly created scope or the name of a registered controller if passed as a string. 1551 * - `controllerAs` - `{string=}` A controller alias name. If present the controller will be 1552 * published to scope under the `controllerAs` name. 1553 * - `template` - `{string=}` An html template as a string. 1554 * - `templateUrl` - `{string=}` A path to an html template. 1555 * - `transformTemplate` - `{function(template)=}` A function which transforms the template after 1556 * it is loaded. It will be given the template string as a parameter, and should 1557 * return a a new string representing the transformed template. 1558 * - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should 1559 * be injected into the controller. If any of these dependencies are promises, the compiler 1560 * will wait for them all to be resolved, or if one is rejected before the controller is 1561 * instantiated `compile()` will fail.. 1562 * * `key` - `{string}`: a name of a dependency to be injected into the controller. 1563 * * `factory` - `{string|function}`: If `string` then it is an alias for a service. 1564 * Otherwise if function, then it is injected and the return value is treated as the 1565 * dependency. If the result is a promise, it is resolved before its value is 1566 * injected into the controller. 1567 * 1568 * @returns {object=} promise A promise, which will be resolved with a `compileData` object. 1569 * `compileData` has the following properties: 1570 * 1571 * - `element` - `{element}`: an uncompiled element matching the provided template. 1572 * - `link` - `{function(scope)}`: A link function, which, when called, will compile 1573 * the element and instantiate the provided controller (if given). 1574 * - `locals` - `{object}`: The locals which will be passed into the controller once `link` is 1575 * called. If `bindToController` is true, they will be coppied to the ctrl instead 1576 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. 1577 */ 1578 this.compile = function(options) { 1579 var templateUrl = options.templateUrl; 1580 var template = options.template || ''; 1581 var controller = options.controller; 1582 var controllerAs = options.controllerAs; 1583 var resolve = angular.extend({}, options.resolve || {}); 1584 var locals = angular.extend({}, options.locals || {}); 1585 var transformTemplate = options.transformTemplate || angular.identity; 1586 var bindToController = options.bindToController; 1587 1588 // Take resolve values and invoke them. 1589 // Resolves can either be a string (value: 'MyRegisteredAngularConst'), 1590 // or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {}) 1591 angular.forEach(resolve, function(value, key) { 1592 if (angular.isString(value)) { 1593 resolve[key] = $injector.get(value); 1594 } else { 1595 resolve[key] = $injector.invoke(value); 1596 } 1597 }); 1598 //Add the locals, which are just straight values to inject 1599 //eg locals: { three: 3 }, will inject three into the controller 1600 angular.extend(resolve, locals); 1601 1602 if (templateUrl) { 1603 resolve.$template = $http.get(templateUrl, {cache: $templateCache}) 1604 .then(function(response) { 1605 return response.data; 1606 }); 1607 } else { 1608 resolve.$template = $q.when(template); 1609 } 1610 1611 // Wait for all the resolves to finish if they are promises 1612 return $q.all(resolve).then(function(locals) { 1613 1614 var compiledData; 1615 var template = transformTemplate(locals.$template, options); 1616 var element = options.element || angular.element('<div>').html(template.trim()).contents(); 1617 var linkFn = $compile(element); 1618 1619 // Return a linking function that can be used later when the element is ready 1620 return compiledData = { 1621 locals: locals, 1622 element: element, 1623 link: function link(scope) { 1624 locals.$scope = scope; 1625 1626 //Instantiate controller if it exists, because we have scope 1627 if (controller) { 1628 var invokeCtrl = $controller(controller, locals, true); 1629 if (bindToController) { 1630 angular.extend(invokeCtrl.instance, locals); 1631 } 1632 var ctrl = invokeCtrl(); 1633 //See angular-route source for this logic 1634 element.data('$ngControllerController', ctrl); 1635 element.children().data('$ngControllerController', ctrl); 1636 1637 if (controllerAs) { 1638 scope[controllerAs] = ctrl; 1639 } 1640 1641 // Publish reference to this controller 1642 compiledData.controller = ctrl; 1643 } 1644 return linkFn(scope); 1645 } 1646 }; 1647 }); 1648 1649 }; 1650 } 1651 mdCompilerService.$inject = ["$q", "$http", "$injector", "$compile", "$controller", "$templateCache"]; 1652 1653 })(); 1654 (function(){ 1655 "use strict"; 1656 1657 var HANDLERS = {}; 1658 1659 /* The state of the current 'pointer' 1660 * The pointer represents the state of the current touch. 1661 * It contains normalized x and y coordinates from DOM events, 1662 * as well as other information abstracted from the DOM. 1663 */ 1664 1665 var pointer, lastPointer, forceSkipClickHijack = false; 1666 1667 /** 1668 * The position of the most recent click if that click was on a label element. 1669 * @type {{x: number, y: number}?} 1670 */ 1671 var lastLabelClickPos = null; 1672 1673 // Used to attach event listeners once when multiple ng-apps are running. 1674 var isInitialized = false; 1675 1676 angular 1677 .module('material.core.gestures', [ ]) 1678 .provider('$mdGesture', MdGestureProvider) 1679 .factory('$$MdGestureHandler', MdGestureHandler) 1680 .run( attachToDocument ); 1681 1682 /** 1683 * @ngdoc service 1684 * @name $mdGestureProvider 1685 * @module material.core.gestures 1686 * 1687 * @description 1688 * In some scenarios on Mobile devices (without jQuery), the click events should NOT be hijacked. 1689 * `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile 1690 * devices. 1691 * 1692 * <hljs lang="js"> 1693 * app.config(function($mdGestureProvider) { 1694 * 1695 * // For mobile devices without jQuery loaded, do not 1696 * // intercept click events during the capture phase. 1697 * $mdGestureProvider.skipClickHijack(); 1698 * 1699 * }); 1700 * </hljs> 1701 * 1702 */ 1703 function MdGestureProvider() { } 1704 1705 MdGestureProvider.prototype = { 1706 1707 // Publish access to setter to configure a variable BEFORE the 1708 // $mdGesture service is instantiated... 1709 skipClickHijack: function() { 1710 return forceSkipClickHijack = true; 1711 }, 1712 1713 /** 1714 * $get is used to build an instance of $mdGesture 1715 * @ngInject 1716 */ 1717 $get : ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) { 1718 return new MdGesture($$MdGestureHandler, $$rAF, $timeout); 1719 }] 1720 }; 1721 1722 1723 1724 /** 1725 * MdGesture factory construction function 1726 * @ngInject 1727 */ 1728 function MdGesture($$MdGestureHandler, $$rAF, $timeout) { 1729 var userAgent = navigator.userAgent || navigator.vendor || window.opera; 1730 var isIos = userAgent.match(/ipad|iphone|ipod/i); 1731 var isAndroid = userAgent.match(/android/i); 1732 var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); 1733 1734 var self = { 1735 handler: addHandler, 1736 register: register, 1737 // On mobile w/out jQuery, we normally intercept clicks. Should we skip that? 1738 isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack 1739 }; 1740 1741 if (self.isHijackingClicks) { 1742 var maxClickDistance = 6; 1743 self.handler('click', { 1744 options: { 1745 maxDistance: maxClickDistance 1746 }, 1747 onEnd: checkDistanceAndEmit('click') 1748 }); 1749 1750 self.handler('focus', { 1751 options: { 1752 maxDistance: maxClickDistance 1753 }, 1754 onEnd: function(ev, pointer) { 1755 if (pointer.distance < this.state.options.maxDistance) { 1756 if (canFocus(ev.target)) { 1757 this.dispatchEvent(ev, 'focus', pointer); 1758 ev.target.focus(); 1759 } 1760 } 1761 1762 function canFocus(element) { 1763 var focusableElements = ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA', 'VIDEO', 'AUDIO']; 1764 1765 return (element.getAttribute('tabindex') != '-1') && 1766 !element.hasAttribute('DISABLED') && 1767 (element.hasAttribute('tabindex') || element.hasAttribute('href') || 1768 (focusableElements.indexOf(element.nodeName) != -1)); 1769 } 1770 } 1771 }); 1772 1773 self.handler('mouseup', { 1774 options: { 1775 maxDistance: maxClickDistance 1776 }, 1777 onEnd: checkDistanceAndEmit('mouseup') 1778 }); 1779 1780 self.handler('mousedown', { 1781 onStart: function(ev) { 1782 this.dispatchEvent(ev, 'mousedown'); 1783 } 1784 }); 1785 } 1786 1787 function checkDistanceAndEmit(eventName) { 1788 return function(ev, pointer) { 1789 if (pointer.distance < this.state.options.maxDistance) { 1790 this.dispatchEvent(ev, eventName, pointer); 1791 } 1792 }; 1793 } 1794 1795 /* 1796 * Register an element to listen for a handler. 1797 * This allows an element to override the default options for a handler. 1798 * Additionally, some handlers like drag and hold only dispatch events if 1799 * the domEvent happens inside an element that's registered to listen for these events. 1800 * 1801 * @see GestureHandler for how overriding of default options works. 1802 * @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horziontal: false }) 1803 */ 1804 function register(element, handlerName, options) { 1805 var handler = HANDLERS[handlerName.replace(/^\$md./, '')]; 1806 if (!handler) { 1807 throw new Error('Failed to register element with handler ' + handlerName + '. ' + 1808 'Available handlers: ' + Object.keys(HANDLERS).join(', ')); 1809 } 1810 return handler.registerElement(element, options); 1811 } 1812 1813 /* 1814 * add a handler to $mdGesture. see below. 1815 */ 1816 function addHandler(name, definition) { 1817 var handler = new $$MdGestureHandler(name); 1818 angular.extend(handler, definition); 1819 HANDLERS[name] = handler; 1820 1821 return self; 1822 } 1823 1824 /* 1825 * Register handlers. These listen to touch/start/move events, interpret them, 1826 * and dispatch gesture events depending on options & conditions. These are all 1827 * instances of GestureHandler. 1828 * @see GestureHandler 1829 */ 1830 return self 1831 /* 1832 * The press handler dispatches an event on touchdown/touchend. 1833 * It's a simple abstraction of touch/mouse/pointer start and end. 1834 */ 1835 .handler('press', { 1836 onStart: function (ev, pointer) { 1837 this.dispatchEvent(ev, '$md.pressdown'); 1838 }, 1839 onEnd: function (ev, pointer) { 1840 this.dispatchEvent(ev, '$md.pressup'); 1841 } 1842 }) 1843 1844 /* 1845 * The hold handler dispatches an event if the user keeps their finger within 1846 * the same <maxDistance> area for <delay> ms. 1847 * The hold handler will only run if a parent of the touch target is registered 1848 * to listen for hold events through $mdGesture.register() 1849 */ 1850 .handler('hold', { 1851 options: { 1852 maxDistance: 6, 1853 delay: 500 1854 }, 1855 onCancel: function () { 1856 $timeout.cancel(this.state.timeout); 1857 }, 1858 onStart: function (ev, pointer) { 1859 // For hold, require a parent to be registered with $mdGesture.register() 1860 // Because we prevent scroll events, this is necessary. 1861 if (!this.state.registeredParent) return this.cancel(); 1862 1863 this.state.pos = {x: pointer.x, y: pointer.y}; 1864 this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() { 1865 this.dispatchEvent(ev, '$md.hold'); 1866 this.cancel(); //we're done! 1867 }), this.state.options.delay, false); 1868 }, 1869 onMove: function (ev, pointer) { 1870 // Don't scroll while waiting for hold. 1871 // If we don't preventDefault touchmove events here, Android will assume we don't 1872 // want to listen to anymore touch events. It will start scrolling and stop sending 1873 // touchmove events. 1874 ev.preventDefault(); 1875 1876 // If the user moves greater than <maxDistance> pixels, stop the hold timer 1877 // set in onStart 1878 var dx = this.state.pos.x - pointer.x; 1879 var dy = this.state.pos.y - pointer.y; 1880 if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) { 1881 this.cancel(); 1882 } 1883 }, 1884 onEnd: function () { 1885 this.onCancel(); 1886 } 1887 }) 1888 1889 /* 1890 * The drag handler dispatches a drag event if the user holds and moves his finger greater than 1891 * <minDistance> px in the x or y direction, depending on options.horizontal. 1892 * The drag will be cancelled if the user moves his finger greater than <minDistance>*<cancelMultiplier> in 1893 * the perpendicular direction. Eg if the drag is horizontal and the user moves his finger <minDistance>*<cancelMultiplier> 1894 * pixels vertically, this handler won't consider the move part of a drag. 1895 */ 1896 .handler('drag', { 1897 options: { 1898 minDistance: 6, 1899 horizontal: true, 1900 cancelMultiplier: 1.5 1901 }, 1902 onStart: function (ev) { 1903 // For drag, require a parent to be registered with $mdGesture.register() 1904 if (!this.state.registeredParent) this.cancel(); 1905 }, 1906 onMove: function (ev, pointer) { 1907 var shouldStartDrag, shouldCancel; 1908 // Don't scroll while deciding if this touchmove qualifies as a drag event. 1909 // If we don't preventDefault touchmove events here, Android will assume we don't 1910 // want to listen to anymore touch events. It will start scrolling and stop sending 1911 // touchmove events. 1912 ev.preventDefault(); 1913 1914 if (!this.state.dragPointer) { 1915 if (this.state.options.horizontal) { 1916 shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance; 1917 shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier; 1918 } else { 1919 shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance; 1920 shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier; 1921 } 1922 1923 if (shouldStartDrag) { 1924 // Create a new pointer representing this drag, starting at this point where the drag started. 1925 this.state.dragPointer = makeStartPointer(ev); 1926 updatePointerState(ev, this.state.dragPointer); 1927 this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer); 1928 1929 } else if (shouldCancel) { 1930 this.cancel(); 1931 } 1932 } else { 1933 this.dispatchDragMove(ev); 1934 } 1935 }, 1936 // Only dispatch dragmove events every frame; any more is unnecessray 1937 dispatchDragMove: $$rAF.throttle(function (ev) { 1938 // Make sure the drag didn't stop while waiting for the next frame 1939 if (this.state.isRunning) { 1940 updatePointerState(ev, this.state.dragPointer); 1941 this.dispatchEvent(ev, '$md.drag', this.state.dragPointer); 1942 } 1943 }), 1944 onEnd: function (ev, pointer) { 1945 if (this.state.dragPointer) { 1946 updatePointerState(ev, this.state.dragPointer); 1947 this.dispatchEvent(ev, '$md.dragend', this.state.dragPointer); 1948 } 1949 } 1950 }) 1951 1952 /* 1953 * The swipe handler will dispatch a swipe event if, on the end of a touch, 1954 * the velocity and distance were high enough. 1955 */ 1956 .handler('swipe', { 1957 options: { 1958 minVelocity: 0.65, 1959 minDistance: 10 1960 }, 1961 onEnd: function (ev, pointer) { 1962 var eventType; 1963 1964 if (Math.abs(pointer.velocityX) > this.state.options.minVelocity && 1965 Math.abs(pointer.distanceX) > this.state.options.minDistance) { 1966 eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight'; 1967 this.dispatchEvent(ev, eventType); 1968 } 1969 else if (Math.abs(pointer.velocityY) > this.state.options.minVelocity && 1970 Math.abs(pointer.distanceY) > this.state.options.minDistance) { 1971 eventType = pointer.directionY == 'up' ? '$md.swipeup' : '$md.swipedown'; 1972 this.dispatchEvent(ev, eventType); 1973 } 1974 } 1975 }); 1976 1977 } 1978 MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"]; 1979 1980 /** 1981 * MdGestureHandler 1982 * A GestureHandler is an object which is able to dispatch custom dom events 1983 * based on native dom {touch,pointer,mouse}{start,move,end} events. 1984 * 1985 * A gesture will manage its lifecycle through the start,move,end, and cancel 1986 * functions, which are called by native dom events. 1987 * 1988 * A gesture has the concept of 'options' (eg a swipe's required velocity), which can be 1989 * overridden by elements registering through $mdGesture.register() 1990 */ 1991 function GestureHandler (name) { 1992 this.name = name; 1993 this.state = {}; 1994 } 1995 1996 function MdGestureHandler() { 1997 var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); 1998 1999 GestureHandler.prototype = { 2000 options: {}, 2001 // jQuery listeners don't work with custom DOMEvents, so we have to dispatch events 2002 // differently when jQuery is loaded 2003 dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent, 2004 2005 // These are overridden by the registered handler 2006 onStart: angular.noop, 2007 onMove: angular.noop, 2008 onEnd: angular.noop, 2009 onCancel: angular.noop, 2010 2011 // onStart sets up a new state for the handler, which includes options from the 2012 // nearest registered parent element of ev.target. 2013 start: function (ev, pointer) { 2014 if (this.state.isRunning) return; 2015 var parentTarget = this.getNearestParent(ev.target); 2016 // Get the options from the nearest registered parent 2017 var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {}; 2018 2019 this.state = { 2020 isRunning: true, 2021 // Override the default options with the nearest registered parent's options 2022 options: angular.extend({}, this.options, parentTargetOptions), 2023 // Pass in the registered parent node to the state so the onStart listener can use 2024 registeredParent: parentTarget 2025 }; 2026 this.onStart(ev, pointer); 2027 }, 2028 move: function (ev, pointer) { 2029 if (!this.state.isRunning) return; 2030 this.onMove(ev, pointer); 2031 }, 2032 end: function (ev, pointer) { 2033 if (!this.state.isRunning) return; 2034 this.onEnd(ev, pointer); 2035 this.state.isRunning = false; 2036 }, 2037 cancel: function (ev, pointer) { 2038 this.onCancel(ev, pointer); 2039 this.state = {}; 2040 }, 2041 2042 // Find and return the nearest parent element that has been registered to 2043 // listen for this handler via $mdGesture.register(element, 'handlerName'). 2044 getNearestParent: function (node) { 2045 var current = node; 2046 while (current) { 2047 if ((current.$mdGesture || {})[this.name]) { 2048 return current; 2049 } 2050 current = current.parentNode; 2051 } 2052 return null; 2053 }, 2054 2055 // Called from $mdGesture.register when an element registers itself with a handler. 2056 // Store the options the user gave on the DOMElement itself. These options will 2057 // be retrieved with getNearestParent when the handler starts. 2058 registerElement: function (element, options) { 2059 var self = this; 2060 element[0].$mdGesture = element[0].$mdGesture || {}; 2061 element[0].$mdGesture[this.name] = options || {}; 2062 element.on('$destroy', onDestroy); 2063 2064 return onDestroy; 2065 2066 function onDestroy() { 2067 delete element[0].$mdGesture[self.name]; 2068 element.off('$destroy', onDestroy); 2069 } 2070 } 2071 }; 2072 2073 return GestureHandler; 2074 2075 /* 2076 * Dispatch an event with jQuery 2077 * TODO: Make sure this sends bubbling events 2078 * 2079 * @param srcEvent the original DOM touch event that started this. 2080 * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') 2081 * @param eventPointer the pointer object that matches this event. 2082 */ 2083 function jQueryDispatchEvent(srcEvent, eventType, eventPointer) { 2084 eventPointer = eventPointer || pointer; 2085 var eventObj = new angular.element.Event(eventType); 2086 2087 eventObj.$material = true; 2088 eventObj.pointer = eventPointer; 2089 eventObj.srcEvent = srcEvent; 2090 2091 angular.extend(eventObj, { 2092 clientX: eventPointer.x, 2093 clientY: eventPointer.y, 2094 screenX: eventPointer.x, 2095 screenY: eventPointer.y, 2096 pageX: eventPointer.x, 2097 pageY: eventPointer.y, 2098 ctrlKey: srcEvent.ctrlKey, 2099 altKey: srcEvent.altKey, 2100 shiftKey: srcEvent.shiftKey, 2101 metaKey: srcEvent.metaKey 2102 }); 2103 angular.element(eventPointer.target).trigger(eventObj); 2104 } 2105 2106 /* 2107 * NOTE: nativeDispatchEvent is very performance sensitive. 2108 * @param srcEvent the original DOM touch event that started this. 2109 * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') 2110 * @param eventPointer the pointer object that matches this event. 2111 */ 2112 function nativeDispatchEvent(srcEvent, eventType, eventPointer) { 2113 eventPointer = eventPointer || pointer; 2114 var eventObj; 2115 2116 if (eventType === 'click' || eventType == 'mouseup' || eventType == 'mousedown' ) { 2117 eventObj = document.createEvent('MouseEvents'); 2118 eventObj.initMouseEvent( 2119 eventType, true, true, window, srcEvent.detail, 2120 eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y, 2121 srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey, 2122 srcEvent.button, srcEvent.relatedTarget || null 2123 ); 2124 2125 } else { 2126 eventObj = document.createEvent('CustomEvent'); 2127 eventObj.initCustomEvent(eventType, true, true, {}); 2128 } 2129 eventObj.$material = true; 2130 eventObj.pointer = eventPointer; 2131 eventObj.srcEvent = srcEvent; 2132 eventPointer.target.dispatchEvent(eventObj); 2133 } 2134 2135 } 2136 2137 /** 2138 * Attach Gestures: hook document and check shouldHijack clicks 2139 * @ngInject 2140 */ 2141 function attachToDocument( $mdGesture, $$MdGestureHandler ) { 2142 2143 // Polyfill document.contains for IE11. 2144 // TODO: move to util 2145 document.contains || (document.contains = function (node) { 2146 return document.body.contains(node); 2147 }); 2148 2149 if (!isInitialized && $mdGesture.isHijackingClicks ) { 2150 /* 2151 * If hijack clicks is true, we preventDefault any click that wasn't 2152 * sent by ngMaterial. This is because on older Android & iOS, a false, or 'ghost', 2153 * click event will be sent ~400ms after a touchend event happens. 2154 * The only way to know if this click is real is to prevent any normal 2155 * click events, and add a flag to events sent by material so we know not to prevent those. 2156 * 2157 * Two exceptions to click events that should be prevented are: 2158 * - click events sent by the keyboard (eg form submit) 2159 * - events that originate from an Ionic app 2160 */ 2161 document.addEventListener('click' , clickHijacker , true); 2162 document.addEventListener('mouseup' , mouseInputHijacker, true); 2163 document.addEventListener('mousedown', mouseInputHijacker, true); 2164 document.addEventListener('focus' , mouseInputHijacker, true); 2165 2166 isInitialized = true; 2167 } 2168 2169 function mouseInputHijacker(ev) { 2170 var isKeyClick = !ev.clientX && !ev.clientY; 2171 if (!isKeyClick && !ev.$material && !ev.isIonicTap 2172 && !isInputEventFromLabelClick(ev)) { 2173 ev.preventDefault(); 2174 ev.stopPropagation(); 2175 } 2176 } 2177 2178 function clickHijacker(ev) { 2179 var isKeyClick = ev.clientX === 0 && ev.clientY === 0; 2180 if (!isKeyClick && !ev.$material && !ev.isIonicTap 2181 && !isInputEventFromLabelClick(ev)) { 2182 ev.preventDefault(); 2183 ev.stopPropagation(); 2184 lastLabelClickPos = null; 2185 } else { 2186 lastLabelClickPos = null; 2187 if (ev.target.tagName.toLowerCase() == 'label') { 2188 lastLabelClickPos = {x: ev.x, y: ev.y}; 2189 } 2190 } 2191 } 2192 2193 2194 // Listen to all events to cover all platforms. 2195 var START_EVENTS = 'mousedown touchstart pointerdown'; 2196 var MOVE_EVENTS = 'mousemove touchmove pointermove'; 2197 var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel'; 2198 2199 angular.element(document) 2200 .on(START_EVENTS, gestureStart) 2201 .on(MOVE_EVENTS, gestureMove) 2202 .on(END_EVENTS, gestureEnd) 2203 // For testing 2204 .on('$$mdGestureReset', function gestureClearCache () { 2205 lastPointer = pointer = null; 2206 }); 2207 2208 /* 2209 * When a DOM event happens, run all registered gesture handlers' lifecycle 2210 * methods which match the DOM event. 2211 * Eg when a 'touchstart' event happens, runHandlers('start') will call and 2212 * run `handler.cancel()` and `handler.start()` on all registered handlers. 2213 */ 2214 function runHandlers(handlerEvent, event) { 2215 var handler; 2216 for (var name in HANDLERS) { 2217 handler = HANDLERS[name]; 2218 if( handler instanceof $$MdGestureHandler ) { 2219 2220 if (handlerEvent === 'start') { 2221 // Run cancel to reset any handlers' state 2222 handler.cancel(); 2223 } 2224 handler[handlerEvent](event, pointer); 2225 2226 } 2227 } 2228 } 2229 2230 /* 2231 * gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android) 2232 * If it is legitimate, we initiate the pointer state and mark the current pointer's type 2233 * For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events 2234 * won't effect it. 2235 */ 2236 function gestureStart(ev) { 2237 // If we're already touched down, abort 2238 if (pointer) return; 2239 2240 var now = +Date.now(); 2241 2242 // iOS & old android bug: after a touch event, a click event is sent 350 ms later. 2243 // If <400ms have passed, don't allow an event of a different type than the previous event 2244 if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) { 2245 return; 2246 } 2247 2248 pointer = makeStartPointer(ev); 2249 2250 runHandlers('start', ev); 2251 } 2252 /* 2253 * If a move event happens of the right type, update the pointer and run all the move handlers. 2254 * "of the right type": if a mousemove happens but our pointer started with a touch event, do nothing. 2255 */ 2256 function gestureMove(ev) { 2257 if (!pointer || !typesMatch(ev, pointer)) return; 2258 2259 updatePointerState(ev, pointer); 2260 runHandlers('move', ev); 2261 } 2262 /* 2263 * If an end event happens of the right type, update the pointer, run endHandlers, and save the pointer as 'lastPointer' 2264 */ 2265 function gestureEnd(ev) { 2266 if (!pointer || !typesMatch(ev, pointer)) return; 2267 2268 updatePointerState(ev, pointer); 2269 pointer.endTime = +Date.now(); 2270 2271 runHandlers('end', ev); 2272 2273 lastPointer = pointer; 2274 pointer = null; 2275 } 2276 2277 } 2278 attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"]; 2279 2280 // ******************** 2281 // Module Functions 2282 // ******************** 2283 2284 /* 2285 * Initiate the pointer. x, y, and the pointer's type. 2286 */ 2287 function makeStartPointer(ev) { 2288 var point = getEventPoint(ev); 2289 var startPointer = { 2290 startTime: +Date.now(), 2291 target: ev.target, 2292 // 'p' for pointer events, 'm' for mouse, 't' for touch 2293 type: ev.type.charAt(0) 2294 }; 2295 startPointer.startX = startPointer.x = point.pageX; 2296 startPointer.startY = startPointer.y = point.pageY; 2297 return startPointer; 2298 } 2299 2300 /* 2301 * return whether the pointer's type matches the event's type. 2302 * Eg if a touch event happens but the pointer has a mouse type, return false. 2303 */ 2304 function typesMatch(ev, pointer) { 2305 return ev && pointer && ev.type.charAt(0) === pointer.type; 2306 } 2307 2308 /** 2309 * Gets whether the given event is an input event that was caused by clicking on an 2310 * associated label element. 2311 * 2312 * This is necessary because the browser will, upon clicking on a label element, fire an 2313 * *extra* click event on its associated input (if any). mdGesture is able to flag the label 2314 * click as with `$material` correctly, but not the second input click. 2315 * 2316 * In order to determine whether an input event is from a label click, we compare the (x, y) for 2317 * the event to the (x, y) for the most recent label click (which is cleared whenever a non-label 2318 * click occurs). Unfortunately, there are no event properties that tie the input and the label 2319 * together (such as relatedTarget). 2320 * 2321 * @param {MouseEvent} event 2322 * @returns {boolean} 2323 */ 2324 function isInputEventFromLabelClick(event) { 2325 return lastLabelClickPos 2326 && lastLabelClickPos.x == event.x 2327 && lastLabelClickPos.y == event.y; 2328 } 2329 2330 /* 2331 * Update the given pointer based upon the given DOMEvent. 2332 * Distance, velocity, direction, duration, etc 2333 */ 2334 function updatePointerState(ev, pointer) { 2335 var point = getEventPoint(ev); 2336 var x = pointer.x = point.pageX; 2337 var y = pointer.y = point.pageY; 2338 2339 pointer.distanceX = x - pointer.startX; 2340 pointer.distanceY = y - pointer.startY; 2341 pointer.distance = Math.sqrt( 2342 pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY 2343 ); 2344 2345 pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : ''; 2346 pointer.directionY = pointer.distanceY > 0 ? 'down' : pointer.distanceY < 0 ? 'up' : ''; 2347 2348 pointer.duration = +Date.now() - pointer.startTime; 2349 pointer.velocityX = pointer.distanceX / pointer.duration; 2350 pointer.velocityY = pointer.distanceY / pointer.duration; 2351 } 2352 2353 /* 2354 * Normalize the point where the DOM event happened whether it's touch or mouse. 2355 * @returns point event obj with pageX and pageY on it. 2356 */ 2357 function getEventPoint(ev) { 2358 ev = ev.originalEvent || ev; // support jQuery events 2359 return (ev.touches && ev.touches[0]) || 2360 (ev.changedTouches && ev.changedTouches[0]) || 2361 ev; 2362 } 2363 2364 })(); 2365 (function(){ 2366 "use strict"; 2367 2368 angular.module('material.core') 2369 .provider('$$interimElement', InterimElementProvider); 2370 2371 /* 2372 * @ngdoc service 2373 * @name $$interimElement 2374 * @module material.core 2375 * 2376 * @description 2377 * 2378 * Factory that contructs `$$interimElement.$service` services. 2379 * Used internally in material design for elements that appear on screen temporarily. 2380 * The service provides a promise-like API for interacting with the temporary 2381 * elements. 2382 * 2383 * ```js 2384 * app.service('$mdToast', function($$interimElement) { 2385 * var $mdToast = $$interimElement(toastDefaultOptions); 2386 * return $mdToast; 2387 * }); 2388 * ``` 2389 * @param {object=} defaultOptions Options used by default for the `show` method on the service. 2390 * 2391 * @returns {$$interimElement.$service} 2392 * 2393 */ 2394 2395 function InterimElementProvider() { 2396 createInterimElementProvider.$get = InterimElementFactory; 2397 InterimElementFactory.$inject = ["$document", "$q", "$$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$injector"]; 2398 return createInterimElementProvider; 2399 2400 /** 2401 * Returns a new provider which allows configuration of a new interimElement 2402 * service. Allows configuration of default options & methods for options, 2403 * as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method) 2404 */ 2405 function createInterimElementProvider(interimFactoryName) { 2406 var EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove']; 2407 2408 var customMethods = {}; 2409 var providerConfig = { 2410 presets: {} 2411 }; 2412 2413 var provider = { 2414 setDefaults: setDefaults, 2415 addPreset: addPreset, 2416 addMethod: addMethod, 2417 $get: factory 2418 }; 2419 2420 /** 2421 * all interim elements will come with the 'build' preset 2422 */ 2423 provider.addPreset('build', { 2424 methods: ['controller', 'controllerAs', 'resolve', 2425 'template', 'templateUrl', 'themable', 'transformTemplate', 'parent'] 2426 }); 2427 2428 factory.$inject = ["$$interimElement", "$injector"]; 2429 return provider; 2430 2431 /** 2432 * Save the configured defaults to be used when the factory is instantiated 2433 */ 2434 function setDefaults(definition) { 2435 providerConfig.optionsFactory = definition.options; 2436 providerConfig.methods = (definition.methods || []).concat(EXPOSED_METHODS); 2437 return provider; 2438 } 2439 2440 /** 2441 * Add a method to the factory that isn't specific to any interim element operations 2442 */ 2443 2444 function addMethod(name, fn) { 2445 customMethods[name] = fn; 2446 return provider; 2447 } 2448 2449 /** 2450 * Save the configured preset to be used when the factory is instantiated 2451 */ 2452 function addPreset(name, definition) { 2453 definition = definition || {}; 2454 definition.methods = definition.methods || []; 2455 definition.options = definition.options || function() { return {}; }; 2456 2457 if (/^cancel|hide|show$/.test(name)) { 2458 throw new Error("Preset '" + name + "' in " + interimFactoryName + " is reserved!"); 2459 } 2460 if (definition.methods.indexOf('_options') > -1) { 2461 throw new Error("Method '_options' in " + interimFactoryName + " is reserved!"); 2462 } 2463 providerConfig.presets[name] = { 2464 methods: definition.methods.concat(EXPOSED_METHODS), 2465 optionsFactory: definition.options, 2466 argOption: definition.argOption 2467 }; 2468 return provider; 2469 } 2470 2471 function addPresetMethod(presetName, methodName, method) { 2472 providerConfig.presets[presetName][methodName] = method; 2473 } 2474 2475 /** 2476 * Create a factory that has the given methods & defaults implementing interimElement 2477 */ 2478 /* @ngInject */ 2479 function factory($$interimElement, $injector) { 2480 var defaultMethods; 2481 var defaultOptions; 2482 var interimElementService = $$interimElement(); 2483 2484 /* 2485 * publicService is what the developer will be using. 2486 * It has methods hide(), cancel(), show(), build(), and any other 2487 * presets which were set during the config phase. 2488 */ 2489 var publicService = { 2490 hide: interimElementService.hide, 2491 cancel: interimElementService.cancel, 2492 show: showInterimElement, 2493 2494 // Special internal method to destroy an interim element without animations 2495 // used when navigation changes causes a $scope.$destroy() action 2496 destroy : destroyInterimElement 2497 }; 2498 2499 2500 defaultMethods = providerConfig.methods || []; 2501 // This must be invoked after the publicService is initialized 2502 defaultOptions = invokeFactory(providerConfig.optionsFactory, {}); 2503 2504 // Copy over the simple custom methods 2505 angular.forEach(customMethods, function(fn, name) { 2506 publicService[name] = fn; 2507 }); 2508 2509 angular.forEach(providerConfig.presets, function(definition, name) { 2510 var presetDefaults = invokeFactory(definition.optionsFactory, {}); 2511 var presetMethods = (definition.methods || []).concat(defaultMethods); 2512 2513 // Every interimElement built with a preset has a field called `$type`, 2514 // which matches the name of the preset. 2515 // Eg in preset 'confirm', options.$type === 'confirm' 2516 angular.extend(presetDefaults, { $type: name }); 2517 2518 // This creates a preset class which has setter methods for every 2519 // method given in the `.addPreset()` function, as well as every 2520 // method given in the `.setDefaults()` function. 2521 // 2522 // @example 2523 // .setDefaults({ 2524 // methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'], 2525 // options: dialogDefaultOptions 2526 // }) 2527 // .addPreset('alert', { 2528 // methods: ['title', 'ok'], 2529 // options: alertDialogOptions 2530 // }) 2531 // 2532 // Set values will be passed to the options when interimElement.show() is called. 2533 function Preset(opts) { 2534 this._options = angular.extend({}, presetDefaults, opts); 2535 } 2536 angular.forEach(presetMethods, function(name) { 2537 Preset.prototype[name] = function(value) { 2538 this._options[name] = value; 2539 return this; 2540 }; 2541 }); 2542 2543 // Create shortcut method for one-linear methods 2544 if (definition.argOption) { 2545 var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1); 2546 publicService[methodName] = function(arg) { 2547 var config = publicService[name](arg); 2548 return publicService.show(config); 2549 }; 2550 } 2551 2552 // eg $mdDialog.alert() will return a new alert preset 2553 publicService[name] = function(arg) { 2554 // If argOption is supplied, eg `argOption: 'content'`, then we assume 2555 // if the argument is not an options object then it is the `argOption` option. 2556 // 2557 // @example `$mdToast.simple('hello')` // sets options.content to hello 2558 // // because argOption === 'content' 2559 if (arguments.length && definition.argOption && 2560 !angular.isObject(arg) && !angular.isArray(arg)) { 2561 2562 return (new Preset())[definition.argOption](arg); 2563 2564 } else { 2565 return new Preset(arg); 2566 } 2567 2568 }; 2569 }); 2570 2571 return publicService; 2572 2573 /** 2574 * 2575 */ 2576 function showInterimElement(opts) { 2577 // opts is either a preset which stores its options on an _options field, 2578 // or just an object made up of options 2579 opts = opts || { }; 2580 if (opts._options) opts = opts._options; 2581 2582 return interimElementService.show( 2583 angular.extend({}, defaultOptions, opts) 2584 ); 2585 } 2586 2587 /** 2588 * Special method to hide and destroy an interimElement WITHOUT 2589 * any 'leave` or hide animations ( an immediate force hide/remove ) 2590 * 2591 * NOTE: This calls the onRemove() subclass method for each component... 2592 * which must have code to respond to `options.$destroy == true` 2593 */ 2594 function destroyInterimElement(opts) { 2595 return interimElementService.destroy(opts); 2596 } 2597 2598 /** 2599 * Helper to call $injector.invoke with a local of the factory name for 2600 * this provider. 2601 * If an $mdDialog is providing options for a dialog and tries to inject 2602 * $mdDialog, a circular dependency error will happen. 2603 * We get around that by manually injecting $mdDialog as a local. 2604 */ 2605 function invokeFactory(factory, defaultVal) { 2606 var locals = {}; 2607 locals[interimFactoryName] = publicService; 2608 return $injector.invoke(factory || function() { return defaultVal; }, {}, locals); 2609 } 2610 2611 } 2612 2613 } 2614 2615 /* @ngInject */ 2616 function InterimElementFactory($document, $q, $$q, $rootScope, $timeout, $rootElement, $animate, 2617 $mdUtil, $mdCompiler, $mdTheming, $injector ) { 2618 return function createInterimElementService() { 2619 var SHOW_CANCELLED = false; 2620 2621 /* 2622 * @ngdoc service 2623 * @name $$interimElement.$service 2624 * 2625 * @description 2626 * A service used to control inserting and removing an element into the DOM. 2627 * 2628 */ 2629 var service, stack = []; 2630 2631 // Publish instance $$interimElement service; 2632 // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect 2633 2634 return service = { 2635 show: show, 2636 hide: hide, 2637 cancel: cancel, 2638 destroy : destroy, 2639 $injector_: $injector 2640 }; 2641 2642 /* 2643 * @ngdoc method 2644 * @name $$interimElement.$service#show 2645 * @kind function 2646 * 2647 * @description 2648 * Adds the `$interimElement` to the DOM and returns a special promise that will be resolved or rejected 2649 * with hide or cancel, respectively. To external cancel/hide, developers should use the 2650 * 2651 * @param {*} options is hashMap of settings 2652 * @returns a Promise 2653 * 2654 */ 2655 function show(options) { 2656 options = options || {}; 2657 var interimElement = new InterimElement(options || {}); 2658 var hideExisting = !options.skipHide && stack.length ? service.hide() : $q.when(true); 2659 2660 // This hide()s only the current interim element before showing the next, new one 2661 // NOTE: this is not reversible (e.g. interim elements are not stackable) 2662 2663 hideExisting.finally(function() { 2664 2665 stack.push(interimElement); 2666 interimElement 2667 .show() 2668 .catch(function( reason ) { 2669 //$log.error("InterimElement.show() error: " + reason ); 2670 return reason; 2671 }); 2672 2673 }); 2674 2675 // Return a promise that will be resolved when the interim 2676 // element is hidden or cancelled... 2677 2678 return interimElement.deferred.promise; 2679 } 2680 2681 /* 2682 * @ngdoc method 2683 * @name $$interimElement.$service#hide 2684 * @kind function 2685 * 2686 * @description 2687 * Removes the `$interimElement` from the DOM and resolves the promise returned from `show` 2688 * 2689 * @param {*} resolveParam Data to resolve the promise with 2690 * @returns a Promise that will be resolved after the element has been removed. 2691 * 2692 */ 2693 function hide(reason, options) { 2694 if ( !stack.length ) return $q.when(reason); 2695 options = options || {}; 2696 2697 if (options.closeAll) { 2698 var promise = $q.all(stack.reverse().map(closeElement)); 2699 stack = []; 2700 return promise; 2701 } else if (options.closeTo !== undefined) { 2702 return $q.all(stack.splice(options.closeTo).map(closeElement)); 2703 } else { 2704 var interim = stack.pop(); 2705 return closeElement(interim); 2706 } 2707 2708 function closeElement(interim) { 2709 interim 2710 .remove(reason, false, options || { }) 2711 .catch(function( reason ) { 2712 //$log.error("InterimElement.hide() error: " + reason ); 2713 return reason; 2714 }); 2715 return interim.deferred.promise; 2716 } 2717 } 2718 2719 /* 2720 * @ngdoc method 2721 * @name $$interimElement.$service#cancel 2722 * @kind function 2723 * 2724 * @description 2725 * Removes the `$interimElement` from the DOM and rejects the promise returned from `show` 2726 * 2727 * @param {*} reason Data to reject the promise with 2728 * @returns Promise that will be resolved after the element has been removed. 2729 * 2730 */ 2731 function cancel(reason, options) { 2732 var interim = stack.pop(); 2733 if ( !interim ) return $q.when(reason); 2734 2735 interim 2736 .remove(reason, true, options || { }) 2737 .catch(function( reason ) { 2738 //$log.error("InterimElement.cancel() error: " + reason ); 2739 return reason; 2740 }); 2741 2742 return interim.deferred.promise; 2743 } 2744 2745 /* 2746 * Special method to quick-remove the interim element without animations 2747 * Note: interim elements are in "interim containers" 2748 */ 2749 function destroy(target) { 2750 var interim = !target ? stack.shift() : null; 2751 var cntr = angular.element(target).length ? angular.element(target)[0].parentNode : null; 2752 2753 if (cntr) { 2754 // Try to find the interim element in the stack which corresponds to the supplied DOM element. 2755 var filtered = stack.filter(function(entry) { 2756 var currNode = entry.options.element[0]; 2757 return (currNode === cntr); 2758 }); 2759 2760 // Note: this function might be called when the element already has been removed, in which 2761 // case we won't find any matches. That's ok. 2762 if (filtered.length > 0) { 2763 interim = filtered[0]; 2764 stack.splice(stack.indexOf(interim), 1); 2765 } 2766 } 2767 2768 return interim ? interim.remove(SHOW_CANCELLED, false, {'$destroy':true}) : $q.when(SHOW_CANCELLED); 2769 } 2770 2771 /* 2772 * Internal Interim Element Object 2773 * Used internally to manage the DOM element and related data 2774 */ 2775 function InterimElement(options) { 2776 var self, element, showAction = $q.when(true); 2777 2778 options = configureScopeAndTransitions(options); 2779 2780 return self = { 2781 options : options, 2782 deferred: $q.defer(), 2783 show : createAndTransitionIn, 2784 remove : transitionOutAndRemove 2785 }; 2786 2787 /** 2788 * Compile, link, and show this interim element 2789 * Use optional autoHided and transition-in effects 2790 */ 2791 function createAndTransitionIn() { 2792 return $q(function(resolve, reject){ 2793 2794 compileElement(options) 2795 .then(function( compiledData ) { 2796 element = linkElement( compiledData, options ); 2797 2798 showAction = showElement(element, options, compiledData.controller) 2799 .then(resolve, rejectAll ); 2800 2801 }, rejectAll); 2802 2803 function rejectAll(fault) { 2804 // Force the '$md<xxx>.show()' promise to reject 2805 self.deferred.reject(fault); 2806 2807 // Continue rejection propagation 2808 reject(fault); 2809 } 2810 }); 2811 } 2812 2813 /** 2814 * After the show process has finished/rejected: 2815 * - announce 'removing', 2816 * - perform the transition-out, and 2817 * - perform optional clean up scope. 2818 */ 2819 function transitionOutAndRemove(response, isCancelled, opts) { 2820 2821 // abort if the show() and compile failed 2822 if ( !element ) return $q.when(false); 2823 2824 options = angular.extend(options || {}, opts || {}); 2825 options.cancelAutoHide && options.cancelAutoHide(); 2826 options.element.triggerHandler('$mdInterimElementRemove'); 2827 2828 if ( options.$destroy === true ) { 2829 2830 return hideElement(options.element, options).then(function(){ 2831 (isCancelled && rejectAll(response)) || resolveAll(response); 2832 }); 2833 2834 } else { 2835 2836 $q.when(showAction) 2837 .finally(function() { 2838 hideElement(options.element, options).then(function() { 2839 2840 (isCancelled && rejectAll(response)) || resolveAll(response); 2841 2842 }, rejectAll); 2843 }); 2844 2845 return self.deferred.promise; 2846 } 2847 2848 2849 /** 2850 * The `show()` returns a promise that will be resolved when the interim 2851 * element is hidden or cancelled... 2852 */ 2853 function resolveAll(response) { 2854 self.deferred.resolve(response); 2855 } 2856 2857 /** 2858 * Force the '$md<xxx>.show()' promise to reject 2859 */ 2860 function rejectAll(fault) { 2861 self.deferred.reject(fault); 2862 } 2863 } 2864 2865 /** 2866 * Prepare optional isolated scope and prepare $animate with default enter and leave 2867 * transitions for the new element instance. 2868 */ 2869 function configureScopeAndTransitions(options) { 2870 options = options || { }; 2871 if ( options.template ) { 2872 options.template = $mdUtil.processTemplate(options.template); 2873 } 2874 2875 return angular.extend({ 2876 preserveScope: false, 2877 cancelAutoHide : angular.noop, 2878 scope: options.scope || $rootScope.$new(options.isolateScope), 2879 2880 /** 2881 * Default usage to enable $animate to transition-in; can be easily overridden via 'options' 2882 */ 2883 onShow: function transitionIn(scope, element, options) { 2884 return $animate.enter(element, options.parent); 2885 }, 2886 2887 /** 2888 * Default usage to enable $animate to transition-out; can be easily overridden via 'options' 2889 */ 2890 onRemove: function transitionOut(scope, element) { 2891 // Element could be undefined if a new element is shown before 2892 // the old one finishes compiling. 2893 return element && $animate.leave(element) || $q.when(); 2894 } 2895 }, options ); 2896 2897 } 2898 2899 /** 2900 * Compile an element with a templateUrl, controller, and locals 2901 */ 2902 function compileElement(options) { 2903 2904 var compiled = !options.skipCompile ? $mdCompiler.compile(options) : null; 2905 2906 return compiled || $q(function (resolve) { 2907 resolve({ 2908 locals: {}, 2909 link: function () { 2910 return options.element; 2911 } 2912 }); 2913 }); 2914 } 2915 2916 /** 2917 * Link an element with compiled configuration 2918 */ 2919 function linkElement(compileData, options){ 2920 angular.extend(compileData.locals, options); 2921 2922 var element = compileData.link(options.scope); 2923 2924 // Search for parent at insertion time, if not specified 2925 options.element = element; 2926 options.parent = findParent(element, options); 2927 if (options.themable) $mdTheming(element); 2928 2929 return element; 2930 } 2931 2932 /** 2933 * Search for parent at insertion time, if not specified 2934 */ 2935 function findParent(element, options) { 2936 var parent = options.parent; 2937 2938 // Search for parent at insertion time, if not specified 2939 if (angular.isFunction(parent)) { 2940 parent = parent(options.scope, element, options); 2941 } else if (angular.isString(parent)) { 2942 parent = angular.element($document[0].querySelector(parent)); 2943 } else { 2944 parent = angular.element(parent); 2945 } 2946 2947 // If parent querySelector/getter function fails, or it's just null, 2948 // find a default. 2949 if (!(parent || {}).length) { 2950 var el; 2951 if ($rootElement[0] && $rootElement[0].querySelector) { 2952 el = $rootElement[0].querySelector(':not(svg) > body'); 2953 } 2954 if (!el) el = $rootElement[0]; 2955 if (el.nodeName == '#comment') { 2956 el = $document[0].body; 2957 } 2958 return angular.element(el); 2959 } 2960 2961 return parent; 2962 } 2963 2964 /** 2965 * If auto-hide is enabled, start timer and prepare cancel function 2966 */ 2967 function startAutoHide() { 2968 var autoHideTimer, cancelAutoHide = angular.noop; 2969 2970 if (options.hideDelay) { 2971 autoHideTimer = $timeout(service.hide, options.hideDelay) ; 2972 cancelAutoHide = function() { 2973 $timeout.cancel(autoHideTimer); 2974 } 2975 } 2976 2977 // Cache for subsequent use 2978 options.cancelAutoHide = function() { 2979 cancelAutoHide(); 2980 options.cancelAutoHide = undefined; 2981 } 2982 } 2983 2984 /** 2985 * Show the element ( with transitions), notify complete and start 2986 * optional auto-Hide 2987 */ 2988 function showElement(element, options, controller) { 2989 // Trigger onShowing callback before the `show()` starts 2990 var notifyShowing = options.onShowing || angular.noop; 2991 // Trigger onComplete callback when the `show()` finishes 2992 var notifyComplete = options.onComplete || angular.noop; 2993 2994 notifyShowing(options.scope, element, options, controller); 2995 2996 return $q(function (resolve, reject) { 2997 try { 2998 // Start transitionIn 2999 $q.when(options.onShow(options.scope, element, options, controller)) 3000 .then(function () { 3001 notifyComplete(options.scope, element, options); 3002 startAutoHide(); 3003 3004 resolve(element); 3005 3006 }, reject ); 3007 3008 } catch(e) { 3009 reject(e.message); 3010 } 3011 }); 3012 } 3013 3014 function hideElement(element, options) { 3015 var announceRemoving = options.onRemoving || angular.noop; 3016 3017 return $$q(function (resolve, reject) { 3018 try { 3019 // Start transitionIn 3020 var action = $$q.when( options.onRemove(options.scope, element, options) || true ); 3021 3022 // Trigger callback *before* the remove operation starts 3023 announceRemoving(element, action); 3024 3025 if ( options.$destroy == true ) { 3026 3027 // For $destroy, onRemove should be synchronous 3028 resolve(element); 3029 3030 } else { 3031 3032 // Wait until transition-out is done 3033 action.then(function () { 3034 3035 if (!options.preserveScope && options.scope ) { 3036 options.scope.$destroy(); 3037 } 3038 3039 resolve(element); 3040 3041 }, reject ); 3042 } 3043 3044 } catch(e) { 3045 reject(e.message); 3046 } 3047 }); 3048 } 3049 3050 } 3051 }; 3052 3053 } 3054 3055 } 3056 3057 })(); 3058 (function(){ 3059 "use strict"; 3060 3061 (function() { 3062 'use strict'; 3063 3064 var $mdUtil, $interpolate, $log; 3065 3066 var SUFFIXES = /(-gt)?-(sm|md|lg|print)/g; 3067 var WHITESPACE = /\s+/g; 3068 3069 var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none', 'noshrink', 'nogrow' ]; 3070 var LAYOUT_OPTIONS = ['row', 'column']; 3071 var ALIGNMENT_MAIN_AXIS= [ "", "start", "center", "end", "stretch", "space-around", "space-between" ]; 3072 var ALIGNMENT_CROSS_AXIS= [ "", "start", "center", "end", "stretch" ]; 3073 3074 var config = { 3075 /** 3076 * Enable directive attribute-to-class conversions 3077 * Developers can use `<body md-layout-css />` to quickly 3078 * disable the Layout directives and prohibit the injection of Layout classNames 3079 */ 3080 enabled: true, 3081 3082 /** 3083 * List of mediaQuery breakpoints and associated suffixes 3084 * 3085 * [ 3086 * { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" }, 3087 * { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" } 3088 * ] 3089 */ 3090 breakpoints: [] 3091 }; 3092 3093 registerLayoutAPI( angular.module('material.core.layout', ['ng']) ); 3094 3095 /** 3096 * registerLayoutAPI() 3097 * 3098 * The original ngMaterial Layout solution used attribute selectors and CSS. 3099 * 3100 * ```html 3101 * <div layout="column"> My Content </div> 3102 * ``` 3103 * 3104 * ```css 3105 * [layout] { 3106 * box-sizing: border-box; 3107 * display:flex; 3108 * } 3109 * [layout=column] { 3110 * flex-direction : column 3111 * } 3112 * ``` 3113 * 3114 * Use of attribute selectors creates significant performance impacts in some 3115 * browsers... mainly IE. 3116 * 3117 * This module registers directives that allow the same layout attributes to be 3118 * interpreted and converted to class selectors. The directive will add equivalent classes to each element that 3119 * contains a Layout directive. 3120 * 3121 * ```html 3122 * <div layout="column" class="layout layout-column"> My Content </div> 3123 *``` 3124 * 3125 * ```css 3126 * .layout { 3127 * box-sizing: border-box; 3128 * display:flex; 3129 * } 3130 * .layout-column { 3131 * flex-direction : column 3132 * } 3133 * ``` 3134 */ 3135 function registerLayoutAPI(module){ 3136 var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; 3137 var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; 3138 3139 // NOTE: these are also defined in constants::MEDIA_PRIORITY and constants::MEDIA 3140 var BREAKPOINTS = [ "", "xs", "gt-xs", "sm", "gt-sm", "md", "gt-md", "lg", "gt-lg", "xl", "print" ]; 3141 var API_WITH_VALUES = [ "layout", "flex", "flex-order", "flex-offset", "layout-align" ]; 3142 var API_NO_VALUES = [ "show", "hide", "layout-padding", "layout-margin" ]; 3143 3144 3145 // Build directive registration functions for the standard Layout API... for all breakpoints. 3146 angular.forEach(BREAKPOINTS, function(mqb) { 3147 3148 // Attribute directives with expected, observable value(s) 3149 angular.forEach( API_WITH_VALUES, function(name){ 3150 var fullName = mqb ? name + "-" + mqb : name; 3151 module.directive( directiveNormalize(fullName), attributeWithObserve(fullName)); 3152 }); 3153 3154 // Attribute directives with no expected value(s) 3155 angular.forEach( API_NO_VALUES, function(name){ 3156 var fullName = mqb ? name + "-" + mqb : name; 3157 module.directive( directiveNormalize(fullName), attributeWithoutValue(fullName)); 3158 }); 3159 3160 }); 3161 3162 // Register other, special directive functions for the Layout features: 3163 module 3164 .directive('mdLayoutCss' , disableLayoutDirective ) 3165 .directive('ngCloak' , buildCloakInterceptor('ng-cloak')) 3166 3167 .directive('layoutWrap' , attributeWithoutValue('layout-wrap')) 3168 .directive('layoutNowrap' , attributeWithoutValue('layout-nowrap')) 3169 .directive('layoutNoWrap' , attributeWithoutValue('layout-no-wrap')) 3170 .directive('layoutFill' , attributeWithoutValue('layout-fill')) 3171 3172 // !! Deprecated attributes: use the `-lt` (aka less-than) notations 3173 3174 .directive('layoutLtMd' , warnAttrNotSupported('layout-lt-md', true)) 3175 .directive('layoutLtLg' , warnAttrNotSupported('layout-lt-lg', true)) 3176 .directive('flexLtMd' , warnAttrNotSupported('flex-lt-md', true)) 3177 .directive('flexLtLg' , warnAttrNotSupported('flex-lt-lg', true)) 3178 3179 .directive('layoutAlignLtMd', warnAttrNotSupported('layout-align-lt-md')) 3180 .directive('layoutAlignLtLg', warnAttrNotSupported('layout-align-lt-lg')) 3181 .directive('flexOrderLtMd' , warnAttrNotSupported('flex-order-lt-md')) 3182 .directive('flexOrderLtLg' , warnAttrNotSupported('flex-order-lt-lg')) 3183 .directive('offsetLtMd' , warnAttrNotSupported('flex-offset-lt-md')) 3184 .directive('offsetLtLg' , warnAttrNotSupported('flex-offset-lt-lg')) 3185 3186 .directive('hideLtMd' , warnAttrNotSupported('hide-lt-md')) 3187 .directive('hideLtLg' , warnAttrNotSupported('hide-lt-lg')) 3188 .directive('showLtMd' , warnAttrNotSupported('show-lt-md')) 3189 .directive('showLtLg' , warnAttrNotSupported('show-lt-lg')); 3190 3191 /** 3192 * Converts snake_case to camelCase. 3193 * Also there is special case for Moz prefix starting with upper case letter. 3194 * @param name Name to normalize 3195 */ 3196 function directiveNormalize(name) { 3197 return name 3198 .replace(PREFIX_REGEXP, '') 3199 .replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { 3200 return offset ? letter.toUpperCase() : letter; 3201 }); 3202 } 3203 3204 } 3205 3206 /** 3207 * Special directive that will disable ALL Layout conversions of layout 3208 * attribute(s) to classname(s). 3209 * 3210 * <link rel="stylesheet" href="angular-material.min.css"> 3211 * <link rel="stylesheet" href="angular-material.layout.css"> 3212 * 3213 * <body md-layout-css> 3214 * ... 3215 * </body> 3216 * 3217 * Note: Using md-layout-css directive requires the developer to load the Material 3218 * Layout Attribute stylesheet (which only uses attribute selectors): 3219 * 3220 * `angular-material.layout.css` 3221 * 3222 * Another option is to use the LayoutProvider to configure and disable the attribute 3223 * conversions; this would obviate the use of the `md-layout-css` directive 3224 * 3225 */ 3226 function disableLayoutDirective() { 3227 return { 3228 restrict : 'A', 3229 priority : '900', 3230 compile : function(element, attr) { 3231 config.enabled = false; 3232 return angular.noop; 3233 } 3234 }; 3235 } 3236 3237 /** 3238 * Tail-hook ngCloak to delay the uncloaking while Layout transformers 3239 * finish processing. Eliminates flicker with Material.Layoouts 3240 */ 3241 function buildCloakInterceptor(className) { 3242 return [ '$timeout', function($timeout){ 3243 return { 3244 restrict : 'A', 3245 priority : -10, // run after normal ng-cloak 3246 compile : function( element ) { 3247 if (!config.enabled) return angular.noop; 3248 3249 // Re-add the cloak 3250 element.addClass(className); 3251 3252 return function( scope, element ) { 3253 // Wait while layout injectors configure, then uncloak 3254 // NOTE: $rAF does not delay enough... and this is a 1x-only event, 3255 // $timeout is acceptable. 3256 $timeout( function(){ 3257 element.removeClass(className); 3258 }, 10, false); 3259 }; 3260 } 3261 }; 3262 }]; 3263 } 3264 3265 3266 // ********************************************************************************* 3267 // 3268 // These functions create registration functions for ngMaterial Layout attribute directives 3269 // This provides easy translation to switch ngMaterial attribute selectors to 3270 // CLASS selectors and directives; which has huge performance implications 3271 // for IE Browsers 3272 // 3273 // ********************************************************************************* 3274 3275 /** 3276 * Creates a directive registration function where a possible dynamic attribute 3277 * value will be observed/watched. 3278 * @param {string} className attribute name; eg `layout-gt-md` with value ="row" 3279 */ 3280 function attributeWithObserve(className) { 3281 3282 return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) { 3283 $mdUtil = _$mdUtil_; 3284 $interpolate = _$interpolate_; 3285 $log = _$log_; 3286 3287 return { 3288 restrict: 'A', 3289 compile: function(element, attr) { 3290 var linkFn; 3291 if (config.enabled) { 3292 // immediately replace static (non-interpolated) invalid values... 3293 3294 validateAttributeUsage(className, attr, element, $log); 3295 3296 validateAttributeValue( className, 3297 getNormalizedAttrValue(className, attr, ""), 3298 buildUpdateFn(element, className, attr) 3299 ); 3300 3301 linkFn = translateWithValueToCssClass; 3302 } 3303 3304 // Use for postLink to account for transforms after ng-transclude. 3305 return linkFn || angular.noop; 3306 } 3307 }; 3308 }]; 3309 3310 /** 3311 * Add as transformed class selector(s), then 3312 * remove the deprecated attribute selector 3313 */ 3314 function translateWithValueToCssClass(scope, element, attrs) { 3315 var updateFn = updateClassWithValue(element, className, attrs); 3316 var unwatch = attrs.$observe(attrs.$normalize(className), updateFn); 3317 3318 updateFn(getNormalizedAttrValue(className, attrs, "")); 3319 scope.$on("$destroy", function() { unwatch() }); 3320 } 3321 } 3322 3323 /** 3324 * Creates a registration function for ngMaterial Layout attribute directive. 3325 * This is a `simple` transpose of attribute usage to class usage; where we ignore 3326 * any attribute value 3327 */ 3328 function attributeWithoutValue(className) { 3329 return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) { 3330 $mdUtil = _$mdUtil_; 3331 $interpolate = _$interpolate_; 3332 $log = _$log_; 3333 3334 return { 3335 restrict: 'A', 3336 compile: function(element, attr) { 3337 var linkFn; 3338 if (config.enabled) { 3339 // immediately replace static (non-interpolated) invalid values... 3340 3341 validateAttributeValue( className, 3342 getNormalizedAttrValue(className, attr, ""), 3343 buildUpdateFn(element, className, attr) 3344 ); 3345 3346 translateToCssClass(null, element); 3347 3348 // Use for postLink to account for transforms after ng-transclude. 3349 linkFn = translateToCssClass; 3350 } 3351 3352 return linkFn || angular.noop; 3353 } 3354 }; 3355 }]; 3356 3357 /** 3358 * Add as transformed class selector, then 3359 * remove the deprecated attribute selector 3360 */ 3361 function translateToCssClass(scope, element) { 3362 element.addClass(className); 3363 } 3364 } 3365 3366 3367 3368 /** 3369 * After link-phase, do NOT remove deprecated layout attribute selector. 3370 * Instead watch the attribute so interpolated data-bindings to layout 3371 * selectors will continue to be supported. 3372 * 3373 * $observe() the className and update with new class (after removing the last one) 3374 * 3375 * e.g. `layout="{{layoutDemo.direction}}"` will update... 3376 * 3377 * NOTE: The value must match one of the specified styles in the CSS. 3378 * For example `flex-gt-md="{{size}}` where `scope.size == 47` will NOT work since 3379 * only breakpoints for 0, 5, 10, 15... 100, 33, 34, 66, 67 are defined. 3380 * 3381 */ 3382 function updateClassWithValue(element, className) { 3383 var lastClass; 3384 3385 return function updateClassFn(newValue) { 3386 var value = validateAttributeValue(className, newValue || ""); 3387 if ( angular.isDefined(value) ) { 3388 if (lastClass) element.removeClass(lastClass); 3389 lastClass = !value ? className : className + "-" + value.replace(WHITESPACE, "-"); 3390 element.addClass(lastClass); 3391 } 3392 }; 3393 } 3394 3395 /** 3396 * Provide console warning that this layout attribute has been deprecated 3397 * 3398 */ 3399 function warnAttrNotSupported(className) { 3400 var parts = className.split("-"); 3401 return ["$log", function($log) { 3402 $log.warn(className + "has been deprecated. Please use a `" + parts[0] + "-gt-<xxx>` variant."); 3403 return angular.noop; 3404 }]; 3405 } 3406 3407 /** 3408 * Centralize warnings for known flexbox issues (especially IE-related issues) 3409 */ 3410 function validateAttributeUsage(className, attr, element, $log){ 3411 var message, usage, url; 3412 var nodeName = element[0].nodeName.toLowerCase(); 3413 3414 switch(className.replace(SUFFIXES,"")) { 3415 case "flex": 3416 if ((nodeName == "md-button") || (nodeName == "fieldset")){ 3417 // @see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers 3418 // Use <div flex> wrapper inside (preferred) or outside 3419 3420 usage = "<" + nodeName + " " + className + "></" + nodeName + ">"; 3421 url = "https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers"; 3422 message = "Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details."; 3423 3424 $log.warn( $mdUtil.supplant(message, [usage, url]) ); 3425 } 3426 } 3427 3428 } 3429 3430 3431 /** 3432 * For the Layout attribute value, validate or replace with default 3433 * fallback value 3434 */ 3435 function validateAttributeValue(className, value, updateFn) { 3436 var origValue = value; 3437 3438 if (!needsInterpolation(value)) { 3439 switch (className.replace(SUFFIXES,"")) { 3440 case 'layout' : 3441 if ( !findIn(value, LAYOUT_OPTIONS) ) { 3442 value = LAYOUT_OPTIONS[0]; // 'row'; 3443 } 3444 break; 3445 3446 case 'flex' : 3447 if (!findIn(value, FLEX_OPTIONS)) { 3448 if (isNaN(value)) { 3449 value = ''; 3450 } 3451 } 3452 break; 3453 3454 case 'flex-offset' : 3455 case 'flex-order' : 3456 if (!value || isNaN(+value)) { 3457 value = '0'; 3458 } 3459 break; 3460 3461 case 'layout-align' : 3462 var axis = extractAlignAxis(value); 3463 value = $mdUtil.supplant("{main}-{cross}",axis); 3464 break; 3465 3466 case 'layout-padding' : 3467 case 'layout-margin' : 3468 case 'layout-fill' : 3469 case 'layout-wrap' : 3470 case 'layout-nowrap' : 3471 case 'layout-nowrap' : 3472 value = ''; 3473 break; 3474 } 3475 3476 if (value != origValue) { 3477 (updateFn || angular.noop)(value); 3478 } 3479 } 3480 3481 return value; 3482 } 3483 3484 /** 3485 * Replace current attribute value with fallback value 3486 */ 3487 function buildUpdateFn(element, className, attrs) { 3488 return function updateAttrValue(fallback) { 3489 if (!needsInterpolation(fallback)) { 3490 // Do not modify the element's attribute value; so 3491 // uses '<ui-layout layout="/api/sidebar.html" />' will not 3492 // be affected. Just update the attrs value. 3493 attrs[attrs.$normalize(className)] = fallback; 3494 } 3495 }; 3496 } 3497 3498 /** 3499 * See if the original value has interpolation symbols: 3500 * e.g. flex-gt-md="{{triggerPoint}}" 3501 */ 3502 function needsInterpolation(value) { 3503 return (value || "").indexOf($interpolate.startSymbol()) > -1; 3504 } 3505 3506 function getNormalizedAttrValue(className, attrs, defaultVal) { 3507 var normalizedAttr = attrs.$normalize(className); 3508 return attrs[normalizedAttr] ? attrs[normalizedAttr].replace(WHITESPACE, "-") : defaultVal || null; 3509 } 3510 3511 function findIn(item, list, replaceWith) { 3512 item = replaceWith && item ? item.replace(WHITESPACE, replaceWith) : item; 3513 3514 var found = false; 3515 if (item) { 3516 list.forEach(function(it) { 3517 it = replaceWith ? it.replace(WHITESPACE, replaceWith) : it; 3518 found = found || (it === item); 3519 }); 3520 } 3521 return found; 3522 } 3523 3524 function extractAlignAxis(attrValue) { 3525 var axis = { 3526 main : "start", 3527 cross: "stretch" 3528 }, values; 3529 3530 attrValue = (attrValue || ""); 3531 3532 if ( attrValue.indexOf("-") == 0 || attrValue.indexOf(" ") == 0) { 3533 // For missing main-axis values 3534 attrValue = "none" + attrValue; 3535 } 3536 3537 values = attrValue.toLowerCase().trim().replace(WHITESPACE, "-").split("-"); 3538 if ( values.length && (values[0] === "space") ) { 3539 // for main-axis values of "space-around" or "space-between" 3540 values = [ values[0]+"-"+values[1],values[2] ]; 3541 } 3542 3543 if ( values.length > 0 ) axis.main = values[0] || axis.main; 3544 if ( values.length > 1 ) axis.cross = values[1] || axis.cross; 3545 3546 if ( ALIGNMENT_MAIN_AXIS.indexOf(axis.main) < 0 ) axis.main = "start"; 3547 if ( ALIGNMENT_CROSS_AXIS.indexOf(axis.cross) < 0 ) axis.cross = "stretch"; 3548 3549 return axis; 3550 } 3551 3552 3553 })(); 3554 3555 })(); 3556 (function(){ 3557 "use strict"; 3558 3559 /** 3560 * @ngdoc module 3561 * @name material.core.componentRegistry 3562 * 3563 * @description 3564 * A component instance registration service. 3565 * Note: currently this as a private service in the SideNav component. 3566 */ 3567 angular.module('material.core') 3568 .factory('$mdComponentRegistry', ComponentRegistry); 3569 3570 /* 3571 * @private 3572 * @ngdoc factory 3573 * @name ComponentRegistry 3574 * @module material.core.componentRegistry 3575 * 3576 */ 3577 function ComponentRegistry($log, $q) { 3578 3579 var self; 3580 var instances = [ ]; 3581 var pendings = { }; 3582 3583 return self = { 3584 /** 3585 * Used to print an error when an instance for a handle isn't found. 3586 */ 3587 notFoundError: function(handle) { 3588 $log.error('No instance found for handle', handle); 3589 }, 3590 /** 3591 * Return all registered instances as an array. 3592 */ 3593 getInstances: function() { 3594 return instances; 3595 }, 3596 3597 /** 3598 * Get a registered instance. 3599 * @param handle the String handle to look up for a registered instance. 3600 */ 3601 get: function(handle) { 3602 if ( !isValidID(handle) ) return null; 3603 3604 var i, j, instance; 3605 for(i = 0, j = instances.length; i < j; i++) { 3606 instance = instances[i]; 3607 if(instance.$$mdHandle === handle) { 3608 return instance; 3609 } 3610 } 3611 return null; 3612 }, 3613 3614 /** 3615 * Register an instance. 3616 * @param instance the instance to register 3617 * @param handle the handle to identify the instance under. 3618 */ 3619 register: function(instance, handle) { 3620 if ( !handle ) return angular.noop; 3621 3622 instance.$$mdHandle = handle; 3623 instances.push(instance); 3624 resolveWhen(); 3625 3626 return deregister; 3627 3628 /** 3629 * Remove registration for an instance 3630 */ 3631 function deregister() { 3632 var index = instances.indexOf(instance); 3633 if (index !== -1) { 3634 instances.splice(index, 1); 3635 } 3636 } 3637 3638 /** 3639 * Resolve any pending promises for this instance 3640 */ 3641 function resolveWhen() { 3642 var dfd = pendings[handle]; 3643 if ( dfd ) { 3644 dfd.resolve( instance ); 3645 delete pendings[handle]; 3646 } 3647 } 3648 }, 3649 3650 /** 3651 * Async accessor to registered component instance 3652 * If not available then a promise is created to notify 3653 * all listeners when the instance is registered. 3654 */ 3655 when : function(handle) { 3656 if ( isValidID(handle) ) { 3657 var deferred = $q.defer(); 3658 var instance = self.get(handle); 3659 3660 if ( instance ) { 3661 deferred.resolve( instance ); 3662 } else { 3663 pendings[handle] = deferred; 3664 } 3665 3666 return deferred.promise; 3667 } 3668 return $q.reject("Invalid `md-component-id` value."); 3669 } 3670 3671 }; 3672 3673 function isValidID(handle){ 3674 return handle && (handle !== ""); 3675 } 3676 3677 } 3678 ComponentRegistry.$inject = ["$log", "$q"]; 3679 3680 })(); 3681 (function(){ 3682 "use strict"; 3683 3684 angular.module('material.core.theming.palette', []) 3685 .constant('$mdColorPalette', { 3686 'red': { 3687 '50': '#ffebee', 3688 '100': '#ffcdd2', 3689 '200': '#ef9a9a', 3690 '300': '#e57373', 3691 '400': '#ef5350', 3692 '500': '#f44336', 3693 '600': '#e53935', 3694 '700': '#d32f2f', 3695 '800': '#c62828', 3696 '900': '#b71c1c', 3697 'A100': '#ff8a80', 3698 'A200': '#ff5252', 3699 'A400': '#ff1744', 3700 'A700': '#d50000', 3701 'contrastDefaultColor': 'light', 3702 'contrastDarkColors': '50 100 200 300 A100', 3703 'contrastStrongLightColors': '400 500 600 700 A200 A400 A700' 3704 }, 3705 'pink': { 3706 '50': '#fce4ec', 3707 '100': '#f8bbd0', 3708 '200': '#f48fb1', 3709 '300': '#f06292', 3710 '400': '#ec407a', 3711 '500': '#e91e63', 3712 '600': '#d81b60', 3713 '700': '#c2185b', 3714 '800': '#ad1457', 3715 '900': '#880e4f', 3716 'A100': '#ff80ab', 3717 'A200': '#ff4081', 3718 'A400': '#f50057', 3719 'A700': '#c51162', 3720 'contrastDefaultColor': 'light', 3721 'contrastDarkColors': '50 100 200 A100', 3722 'contrastStrongLightColors': '500 600 A200 A400 A700' 3723 }, 3724 'purple': { 3725 '50': '#f3e5f5', 3726 '100': '#e1bee7', 3727 '200': '#ce93d8', 3728 '300': '#ba68c8', 3729 '400': '#ab47bc', 3730 '500': '#9c27b0', 3731 '600': '#8e24aa', 3732 '700': '#7b1fa2', 3733 '800': '#6a1b9a', 3734 '900': '#4a148c', 3735 'A100': '#ea80fc', 3736 'A200': '#e040fb', 3737 'A400': '#d500f9', 3738 'A700': '#aa00ff', 3739 'contrastDefaultColor': 'light', 3740 'contrastDarkColors': '50 100 200 A100', 3741 'contrastStrongLightColors': '300 400 A200 A400 A700' 3742 }, 3743 'deep-purple': { 3744 '50': '#ede7f6', 3745 '100': '#d1c4e9', 3746 '200': '#b39ddb', 3747 '300': '#9575cd', 3748 '400': '#7e57c2', 3749 '500': '#673ab7', 3750 '600': '#5e35b1', 3751 '700': '#512da8', 3752 '800': '#4527a0', 3753 '900': '#311b92', 3754 'A100': '#b388ff', 3755 'A200': '#7c4dff', 3756 'A400': '#651fff', 3757 'A700': '#6200ea', 3758 'contrastDefaultColor': 'light', 3759 'contrastDarkColors': '50 100 200 A100', 3760 'contrastStrongLightColors': '300 400 A200' 3761 }, 3762 'indigo': { 3763 '50': '#e8eaf6', 3764 '100': '#c5cae9', 3765 '200': '#9fa8da', 3766 '300': '#7986cb', 3767 '400': '#5c6bc0', 3768 '500': '#3f51b5', 3769 '600': '#3949ab', 3770 '700': '#303f9f', 3771 '800': '#283593', 3772 '900': '#1a237e', 3773 'A100': '#8c9eff', 3774 'A200': '#536dfe', 3775 'A400': '#3d5afe', 3776 'A700': '#304ffe', 3777 'contrastDefaultColor': 'light', 3778 'contrastDarkColors': '50 100 200 A100', 3779 'contrastStrongLightColors': '300 400 A200 A400' 3780 }, 3781 'blue': { 3782 '50': '#e3f2fd', 3783 '100': '#bbdefb', 3784 '200': '#90caf9', 3785 '300': '#64b5f6', 3786 '400': '#42a5f5', 3787 '500': '#2196f3', 3788 '600': '#1e88e5', 3789 '700': '#1976d2', 3790 '800': '#1565c0', 3791 '900': '#0d47a1', 3792 'A100': '#82b1ff', 3793 'A200': '#448aff', 3794 'A400': '#2979ff', 3795 'A700': '#2962ff', 3796 'contrastDefaultColor': 'light', 3797 'contrastDarkColors': '50 100 200 300 400 A100', 3798 'contrastStrongLightColors': '500 600 700 A200 A400 A700' 3799 }, 3800 'light-blue': { 3801 '50': '#e1f5fe', 3802 '100': '#b3e5fc', 3803 '200': '#81d4fa', 3804 '300': '#4fc3f7', 3805 '400': '#29b6f6', 3806 '500': '#03a9f4', 3807 '600': '#039be5', 3808 '700': '#0288d1', 3809 '800': '#0277bd', 3810 '900': '#01579b', 3811 'A100': '#80d8ff', 3812 'A200': '#40c4ff', 3813 'A400': '#00b0ff', 3814 'A700': '#0091ea', 3815 'contrastDefaultColor': 'dark', 3816 'contrastLightColors': '600 700 800 900 A700', 3817 'contrastStrongLightColors': '600 700 800 A700' 3818 }, 3819 'cyan': { 3820 '50': '#e0f7fa', 3821 '100': '#b2ebf2', 3822 '200': '#80deea', 3823 '300': '#4dd0e1', 3824 '400': '#26c6da', 3825 '500': '#00bcd4', 3826 '600': '#00acc1', 3827 '700': '#0097a7', 3828 '800': '#00838f', 3829 '900': '#006064', 3830 'A100': '#84ffff', 3831 'A200': '#18ffff', 3832 'A400': '#00e5ff', 3833 'A700': '#00b8d4', 3834 'contrastDefaultColor': 'dark', 3835 'contrastLightColors': '700 800 900', 3836 'contrastStrongLightColors': '700 800 900' 3837 }, 3838 'teal': { 3839 '50': '#e0f2f1', 3840 '100': '#b2dfdb', 3841 '200': '#80cbc4', 3842 '300': '#4db6ac', 3843 '400': '#26a69a', 3844 '500': '#009688', 3845 '600': '#00897b', 3846 '700': '#00796b', 3847 '800': '#00695c', 3848 '900': '#004d40', 3849 'A100': '#a7ffeb', 3850 'A200': '#64ffda', 3851 'A400': '#1de9b6', 3852 'A700': '#00bfa5', 3853 'contrastDefaultColor': 'dark', 3854 'contrastLightColors': '500 600 700 800 900', 3855 'contrastStrongLightColors': '500 600 700' 3856 }, 3857 'green': { 3858 '50': '#e8f5e9', 3859 '100': '#c8e6c9', 3860 '200': '#a5d6a7', 3861 '300': '#81c784', 3862 '400': '#66bb6a', 3863 '500': '#4caf50', 3864 '600': '#43a047', 3865 '700': '#388e3c', 3866 '800': '#2e7d32', 3867 '900': '#1b5e20', 3868 'A100': '#b9f6ca', 3869 'A200': '#69f0ae', 3870 'A400': '#00e676', 3871 'A700': '#00c853', 3872 'contrastDefaultColor': 'dark', 3873 'contrastLightColors': '600 700 800 900', 3874 'contrastStrongLightColors': '600 700' 3875 }, 3876 'light-green': { 3877 '50': '#f1f8e9', 3878 '100': '#dcedc8', 3879 '200': '#c5e1a5', 3880 '300': '#aed581', 3881 '400': '#9ccc65', 3882 '500': '#8bc34a', 3883 '600': '#7cb342', 3884 '700': '#689f38', 3885 '800': '#558b2f', 3886 '900': '#33691e', 3887 'A100': '#ccff90', 3888 'A200': '#b2ff59', 3889 'A400': '#76ff03', 3890 'A700': '#64dd17', 3891 'contrastDefaultColor': 'dark', 3892 'contrastLightColors': '700 800 900', 3893 'contrastStrongLightColors': '700 800 900' 3894 }, 3895 'lime': { 3896 '50': '#f9fbe7', 3897 '100': '#f0f4c3', 3898 '200': '#e6ee9c', 3899 '300': '#dce775', 3900 '400': '#d4e157', 3901 '500': '#cddc39', 3902 '600': '#c0ca33', 3903 '700': '#afb42b', 3904 '800': '#9e9d24', 3905 '900': '#827717', 3906 'A100': '#f4ff81', 3907 'A200': '#eeff41', 3908 'A400': '#c6ff00', 3909 'A700': '#aeea00', 3910 'contrastDefaultColor': 'dark', 3911 'contrastLightColors': '900', 3912 'contrastStrongLightColors': '900' 3913 }, 3914 'yellow': { 3915 '50': '#fffde7', 3916 '100': '#fff9c4', 3917 '200': '#fff59d', 3918 '300': '#fff176', 3919 '400': '#ffee58', 3920 '500': '#ffeb3b', 3921 '600': '#fdd835', 3922 '700': '#fbc02d', 3923 '800': '#f9a825', 3924 '900': '#f57f17', 3925 'A100': '#ffff8d', 3926 'A200': '#ffff00', 3927 'A400': '#ffea00', 3928 'A700': '#ffd600', 3929 'contrastDefaultColor': 'dark' 3930 }, 3931 'amber': { 3932 '50': '#fff8e1', 3933 '100': '#ffecb3', 3934 '200': '#ffe082', 3935 '300': '#ffd54f', 3936 '400': '#ffca28', 3937 '500': '#ffc107', 3938 '600': '#ffb300', 3939 '700': '#ffa000', 3940 '800': '#ff8f00', 3941 '900': '#ff6f00', 3942 'A100': '#ffe57f', 3943 'A200': '#ffd740', 3944 'A400': '#ffc400', 3945 'A700': '#ffab00', 3946 'contrastDefaultColor': 'dark' 3947 }, 3948 'orange': { 3949 '50': '#fff3e0', 3950 '100': '#ffe0b2', 3951 '200': '#ffcc80', 3952 '300': '#ffb74d', 3953 '400': '#ffa726', 3954 '500': '#ff9800', 3955 '600': '#fb8c00', 3956 '700': '#f57c00', 3957 '800': '#ef6c00', 3958 '900': '#e65100', 3959 'A100': '#ffd180', 3960 'A200': '#ffab40', 3961 'A400': '#ff9100', 3962 'A700': '#ff6d00', 3963 'contrastDefaultColor': 'dark', 3964 'contrastLightColors': '800 900', 3965 'contrastStrongLightColors': '800 900' 3966 }, 3967 'deep-orange': { 3968 '50': '#fbe9e7', 3969 '100': '#ffccbc', 3970 '200': '#ffab91', 3971 '300': '#ff8a65', 3972 '400': '#ff7043', 3973 '500': '#ff5722', 3974 '600': '#f4511e', 3975 '700': '#e64a19', 3976 '800': '#d84315', 3977 '900': '#bf360c', 3978 'A100': '#ff9e80', 3979 'A200': '#ff6e40', 3980 'A400': '#ff3d00', 3981 'A700': '#dd2c00', 3982 'contrastDefaultColor': 'light', 3983 'contrastDarkColors': '50 100 200 300 400 A100 A200', 3984 'contrastStrongLightColors': '500 600 700 800 900 A400 A700' 3985 }, 3986 'brown': { 3987 '50': '#efebe9', 3988 '100': '#d7ccc8', 3989 '200': '#bcaaa4', 3990 '300': '#a1887f', 3991 '400': '#8d6e63', 3992 '500': '#795548', 3993 '600': '#6d4c41', 3994 '700': '#5d4037', 3995 '800': '#4e342e', 3996 '900': '#3e2723', 3997 'A100': '#d7ccc8', 3998 'A200': '#bcaaa4', 3999 'A400': '#8d6e63', 4000 'A700': '#5d4037', 4001 'contrastDefaultColor': 'light', 4002 'contrastDarkColors': '50 100 200', 4003 'contrastStrongLightColors': '300 400' 4004 }, 4005 'grey': { 4006 '50': '#fafafa', 4007 '100': '#f5f5f5', 4008 '200': '#eeeeee', 4009 '300': '#e0e0e0', 4010 '400': '#bdbdbd', 4011 '500': '#9e9e9e', 4012 '600': '#757575', 4013 '700': '#616161', 4014 '800': '#424242', 4015 '900': '#212121', 4016 '1000': '#000000', 4017 'A100': '#ffffff', 4018 'A200': '#eeeeee', 4019 'A400': '#bdbdbd', 4020 'A700': '#616161', 4021 'contrastDefaultColor': 'dark', 4022 'contrastLightColors': '600 700 800 900' 4023 }, 4024 'blue-grey': { 4025 '50': '#eceff1', 4026 '100': '#cfd8dc', 4027 '200': '#b0bec5', 4028 '300': '#90a4ae', 4029 '400': '#78909c', 4030 '500': '#607d8b', 4031 '600': '#546e7a', 4032 '700': '#455a64', 4033 '800': '#37474f', 4034 '900': '#263238', 4035 'A100': '#cfd8dc', 4036 'A200': '#b0bec5', 4037 'A400': '#78909c', 4038 'A700': '#455a64', 4039 'contrastDefaultColor': 'light', 4040 'contrastDarkColors': '50 100 200 300', 4041 'contrastStrongLightColors': '400 500' 4042 } 4043 }); 4044 4045 })(); 4046 (function(){ 4047 "use strict"; 4048 4049 angular.module('material.core.theming', ['material.core.theming.palette']) 4050 .directive('mdTheme', ThemingDirective) 4051 .directive('mdThemable', ThemableDirective) 4052 .provider('$mdTheming', ThemingProvider) 4053 .run(generateAllThemes); 4054 4055 /** 4056 * @ngdoc service 4057 * @name $mdThemingProvider 4058 * @module material.core.theming 4059 * 4060 * @description Provider to configure the `$mdTheming` service. 4061 */ 4062 4063 /** 4064 * @ngdoc method 4065 * @name $mdThemingProvider#setNonce 4066 * @param {string} nonceValue The nonce to be added as an attribute to the theme style tags. 4067 * Setting a value allows the use CSP policy without using the unsafe-inline directive. 4068 */ 4069 4070 /** 4071 * @ngdoc method 4072 * @name $mdThemingProvider#setDefaultTheme 4073 * @param {string} themeName Default theme name to be applied to elements. Default value is `default`. 4074 */ 4075 4076 /** 4077 * @ngdoc method 4078 * @name $mdThemingProvider#alwaysWatchTheme 4079 * @param {boolean} watch Whether or not to always watch themes for changes and re-apply 4080 * classes when they change. Default is `false`. Enabling can reduce performance. 4081 */ 4082 4083 /* Some Example Valid Theming Expressions 4084 * ======================================= 4085 * 4086 * Intention group expansion: (valid for primary, accent, warn, background) 4087 * 4088 * {{primary-100}} - grab shade 100 from the primary palette 4089 * {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7 4090 * {{primary-100-contrast}} - grab shade 100's contrast color 4091 * {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette 4092 * {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1 4093 * {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue 4094 * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules 4095 * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue 4096 * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules 4097 * 4098 * Foreground expansion: Applies rgba to black/white foreground text 4099 * 4100 * {{foreground-1}} - used for primary text 4101 * {{foreground-2}} - used for secondary text/divider 4102 * {{foreground-3}} - used for disabled text 4103 * {{foreground-4}} - used for dividers 4104 * 4105 */ 4106 4107 // In memory generated CSS rules; registered by theme.name 4108 var GENERATED = { }; 4109 4110 // In memory storage of defined themes and color palettes (both loaded by CSS, and user specified) 4111 var PALETTES; 4112 var THEMES; 4113 4114 var DARK_FOREGROUND = { 4115 name: 'dark', 4116 '1': 'rgba(0,0,0,0.87)', 4117 '2': 'rgba(0,0,0,0.54)', 4118 '3': 'rgba(0,0,0,0.26)', 4119 '4': 'rgba(0,0,0,0.12)' 4120 }; 4121 var LIGHT_FOREGROUND = { 4122 name: 'light', 4123 '1': 'rgba(255,255,255,1.0)', 4124 '2': 'rgba(255,255,255,0.7)', 4125 '3': 'rgba(255,255,255,0.3)', 4126 '4': 'rgba(255,255,255,0.12)' 4127 }; 4128 4129 var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)'; 4130 var LIGHT_SHADOW = ''; 4131 4132 var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)'); 4133 var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87)'); 4134 var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)'); 4135 4136 var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background']; 4137 var DEFAULT_COLOR_TYPE = 'primary'; 4138 4139 // A color in a theme will use these hues by default, if not specified by user. 4140 var LIGHT_DEFAULT_HUES = { 4141 'accent': { 4142 'default': 'A200', 4143 'hue-1': 'A100', 4144 'hue-2': 'A400', 4145 'hue-3': 'A700' 4146 }, 4147 'background': { 4148 'default': 'A100', 4149 'hue-1': '300', 4150 'hue-2': '800', 4151 'hue-3': '900' 4152 } 4153 }; 4154 4155 var DARK_DEFAULT_HUES = { 4156 'background': { 4157 'default': '800', 4158 'hue-1': '600', 4159 'hue-2': '300', 4160 'hue-3': '900' 4161 } 4162 }; 4163 THEME_COLOR_TYPES.forEach(function(colorType) { 4164 // Color types with unspecified default hues will use these default hue values 4165 var defaultDefaultHues = { 4166 'default': '500', 4167 'hue-1': '300', 4168 'hue-2': '800', 4169 'hue-3': 'A100' 4170 }; 4171 if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues; 4172 if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues; 4173 }); 4174 4175 var VALID_HUE_VALUES = [ 4176 '50', '100', '200', '300', '400', '500', '600', 4177 '700', '800', '900', 'A100', 'A200', 'A400', 'A700' 4178 ]; 4179 4180 // Whether or not themes are to be generated on-demand (vs. eagerly). 4181 var generateOnDemand = false; 4182 4183 // Nonce to be added as an attribute to the generated themes style tags. 4184 var nonce = null; 4185 4186 function ThemingProvider($mdColorPalette) { 4187 PALETTES = { }; 4188 THEMES = { }; 4189 4190 var themingProvider; 4191 var defaultTheme = 'default'; 4192 var alwaysWatchTheme = false; 4193 4194 // Load JS Defined Palettes 4195 angular.extend(PALETTES, $mdColorPalette); 4196 4197 // Default theme defined in core.js 4198 4199 ThemingService.$inject = ["$rootScope", "$log"]; 4200 return themingProvider = { 4201 definePalette: definePalette, 4202 extendPalette: extendPalette, 4203 theme: registerTheme, 4204 4205 setNonce: function(nonceValue) { 4206 nonce = nonceValue; 4207 }, 4208 setDefaultTheme: function(theme) { 4209 defaultTheme = theme; 4210 }, 4211 alwaysWatchTheme: function(alwaysWatch) { 4212 alwaysWatchTheme = alwaysWatch; 4213 }, 4214 generateThemesOnDemand: function(onDemand) { 4215 generateOnDemand = onDemand; 4216 }, 4217 $get: ThemingService, 4218 _LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES, 4219 _DARK_DEFAULT_HUES: DARK_DEFAULT_HUES, 4220 _PALETTES: PALETTES, 4221 _THEMES: THEMES, 4222 _parseRules: parseRules, 4223 _rgba: rgba 4224 }; 4225 4226 // Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... }); 4227 function definePalette(name, map) { 4228 map = map || {}; 4229 PALETTES[name] = checkPaletteValid(name, map); 4230 return themingProvider; 4231 } 4232 4233 // Returns an new object which is a copy of a given palette `name` with variables from 4234 // `map` overwritten 4235 // Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' }); 4236 function extendPalette(name, map) { 4237 return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map) ); 4238 } 4239 4240 // Make sure that palette has all required hues 4241 function checkPaletteValid(name, map) { 4242 var missingColors = VALID_HUE_VALUES.filter(function(field) { 4243 return !map[field]; 4244 }); 4245 if (missingColors.length) { 4246 throw new Error("Missing colors %1 in palette %2!" 4247 .replace('%1', missingColors.join(', ')) 4248 .replace('%2', name)); 4249 } 4250 4251 return map; 4252 } 4253 4254 // Register a theme (which is a collection of color palettes to use with various states 4255 // ie. warn, accent, primary ) 4256 // Optionally inherit from an existing theme 4257 // $mdThemingProvider.theme('custom-theme').primaryPalette('red'); 4258 function registerTheme(name, inheritFrom) { 4259 if (THEMES[name]) return THEMES[name]; 4260 4261 inheritFrom = inheritFrom || 'default'; 4262 4263 var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom; 4264 var theme = new Theme(name); 4265 4266 if (parentTheme) { 4267 angular.forEach(parentTheme.colors, function(color, colorType) { 4268 theme.colors[colorType] = { 4269 name: color.name, 4270 // Make sure a COPY of the hues is given to the child color, 4271 // not the same reference. 4272 hues: angular.extend({}, color.hues) 4273 }; 4274 }); 4275 } 4276 THEMES[name] = theme; 4277 4278 return theme; 4279 } 4280 4281 function Theme(name) { 4282 var self = this; 4283 self.name = name; 4284 self.colors = {}; 4285 4286 self.dark = setDark; 4287 setDark(false); 4288 4289 function setDark(isDark) { 4290 isDark = arguments.length === 0 ? true : !!isDark; 4291 4292 // If no change, abort 4293 if (isDark === self.isDark) return; 4294 4295 self.isDark = isDark; 4296 4297 self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND; 4298 self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW; 4299 4300 // Light and dark themes have different default hues. 4301 // Go through each existing color type for this theme, and for every 4302 // hue value that is still the default hue value from the previous light/dark setting, 4303 // set it to the default hue value from the new light/dark setting. 4304 var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES; 4305 var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES; 4306 angular.forEach(newDefaultHues, function(newDefaults, colorType) { 4307 var color = self.colors[colorType]; 4308 var oldDefaults = oldDefaultHues[colorType]; 4309 if (color) { 4310 for (var hueName in color.hues) { 4311 if (color.hues[hueName] === oldDefaults[hueName]) { 4312 color.hues[hueName] = newDefaults[hueName]; 4313 } 4314 } 4315 } 4316 }); 4317 4318 return self; 4319 } 4320 4321 THEME_COLOR_TYPES.forEach(function(colorType) { 4322 var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType]; 4323 self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) { 4324 var color = self.colors[colorType] = { 4325 name: paletteName, 4326 hues: angular.extend({}, defaultHues, hues) 4327 }; 4328 4329 Object.keys(color.hues).forEach(function(name) { 4330 if (!defaultHues[name]) { 4331 throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4" 4332 .replace('%1', name) 4333 .replace('%2', self.name) 4334 .replace('%3', paletteName) 4335 .replace('%4', Object.keys(defaultHues).join(', ')) 4336 ); 4337 } 4338 }); 4339 Object.keys(color.hues).map(function(key) { 4340 return color.hues[key]; 4341 }).forEach(function(hueValue) { 4342 if (VALID_HUE_VALUES.indexOf(hueValue) == -1) { 4343 throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5" 4344 .replace('%1', hueValue) 4345 .replace('%2', self.name) 4346 .replace('%3', colorType) 4347 .replace('%4', paletteName) 4348 .replace('%5', VALID_HUE_VALUES.join(', ')) 4349 ); 4350 } 4351 }); 4352 return self; 4353 }; 4354 4355 self[colorType + 'Color'] = function() { 4356 var args = Array.prototype.slice.call(arguments); 4357 console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' + 4358 'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.'); 4359 return self[colorType + 'Palette'].apply(self, args); 4360 }; 4361 }); 4362 } 4363 4364 /** 4365 * @ngdoc service 4366 * @name $mdTheming 4367 * 4368 * @description 4369 * 4370 * Service that makes an element apply theming related classes to itself. 4371 * 4372 * ```js 4373 * app.directive('myFancyDirective', function($mdTheming) { 4374 * return { 4375 * restrict: 'e', 4376 * link: function(scope, el, attrs) { 4377 * $mdTheming(el); 4378 * } 4379 * }; 4380 * }); 4381 * ``` 4382 * @param {el=} element to apply theming to 4383 */ 4384 /* @ngInject */ 4385 function ThemingService($rootScope, $log) { 4386 // Allow us to be invoked via a linking function signature. 4387 var applyTheme = function (scope, el) { 4388 if (el === undefined) { el = scope; scope = undefined; } 4389 if (scope === undefined) { scope = $rootScope; } 4390 applyTheme.inherit(el, el); 4391 }; 4392 4393 applyTheme.THEMES = angular.extend({}, THEMES); 4394 applyTheme.inherit = inheritTheme; 4395 applyTheme.registered = registered; 4396 applyTheme.defaultTheme = function() { return defaultTheme; }; 4397 applyTheme.generateTheme = function(name) { generateTheme(name, nonce); }; 4398 4399 return applyTheme; 4400 4401 /** 4402 * Determine is specified theme name is a valid, registered theme 4403 */ 4404 function registered(themeName) { 4405 if (themeName === undefined || themeName === '') return true; 4406 return applyTheme.THEMES[themeName] !== undefined; 4407 } 4408 4409 /** 4410 * Get theme name for the element, then update with Theme CSS class 4411 */ 4412 function inheritTheme (el, parent) { 4413 var ctrl = parent.controller('mdTheme'); 4414 var attrThemeValue = el.attr('md-theme-watch'); 4415 var watchTheme = (alwaysWatchTheme || angular.isDefined(attrThemeValue)) && attrThemeValue != 'false'; 4416 4417 updateThemeClass(lookupThemeName()); 4418 4419 el.on('$destroy', watchTheme ? $rootScope.$watch(lookupThemeName, updateThemeClass) : angular.noop ); 4420 4421 /** 4422 * Find the theme name from the parent controller or element data 4423 */ 4424 function lookupThemeName() { 4425 // As a few components (dialog) add their controllers later, we should also watch for a controller init. 4426 ctrl = parent.controller('mdTheme') || el.data('$mdThemeController'); 4427 return ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme); 4428 } 4429 4430 /** 4431 * Remove old theme class and apply a new one 4432 * NOTE: if not a valid theme name, then the current name is not changed 4433 */ 4434 function updateThemeClass(theme) { 4435 if (!theme) return; 4436 if (!registered(theme)) { 4437 $log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' + 4438 'Register it with $mdThemingProvider.theme().'); 4439 } 4440 4441 var oldTheme = el.data('$mdThemeName'); 4442 if (oldTheme) el.removeClass('md-' + oldTheme +'-theme'); 4443 el.addClass('md-' + theme + '-theme'); 4444 el.data('$mdThemeName', theme); 4445 if (ctrl) { 4446 el.data('$mdThemeController', ctrl); 4447 } 4448 } 4449 } 4450 4451 } 4452 } 4453 ThemingProvider.$inject = ["$mdColorPalette"]; 4454 4455 function ThemingDirective($mdTheming, $interpolate, $log) { 4456 return { 4457 priority: 100, 4458 link: { 4459 pre: function(scope, el, attrs) { 4460 var ctrl = { 4461 $setTheme: function(theme) { 4462 if (!$mdTheming.registered(theme)) { 4463 $log.warn('attempted to use unregistered theme \'' + theme + '\''); 4464 } 4465 ctrl.$mdTheme = theme; 4466 } 4467 }; 4468 el.data('$mdThemeController', ctrl); 4469 ctrl.$setTheme($interpolate(attrs.mdTheme)(scope)); 4470 attrs.$observe('mdTheme', ctrl.$setTheme); 4471 } 4472 } 4473 }; 4474 } 4475 ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$log"]; 4476 4477 function ThemableDirective($mdTheming) { 4478 return $mdTheming; 4479 } 4480 ThemableDirective.$inject = ["$mdTheming"]; 4481 4482 function parseRules(theme, colorType, rules) { 4483 checkValidPalette(theme, colorType); 4484 4485 rules = rules.replace(/THEME_NAME/g, theme.name); 4486 var generatedRules = []; 4487 var color = theme.colors[colorType]; 4488 4489 var themeNameRegex = new RegExp('.md-' + theme.name + '-theme', 'g'); 4490 // Matches '{{ primary-color }}', etc 4491 var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g'); 4492 var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g; 4493 var palette = PALETTES[color.name]; 4494 4495 // find and replace simple variables where we use a specific hue, not an entire palette 4496 // eg. "{{primary-100}}" 4497 //\(' + THEME_COLOR_TYPES.join('\|') + '\)' 4498 rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) { 4499 if (colorType === 'foreground') { 4500 if (hue == 'shadow') { 4501 return theme.foregroundShadow; 4502 } else { 4503 return theme.foregroundPalette[hue] || theme.foregroundPalette['1']; 4504 } 4505 } 4506 if (hue.indexOf('hue') === 0) { 4507 hue = theme.colors[colorType].hues[hue]; 4508 } 4509 return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity ); 4510 }); 4511 4512 // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3) 4513 angular.forEach(color.hues, function(hueValue, hueName) { 4514 var newRule = rules 4515 .replace(hueRegex, function(match, _, colorType, hueType, opacity) { 4516 return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity); 4517 }); 4518 if (hueName !== 'default') { 4519 newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName); 4520 } 4521 4522 // Don't apply a selector rule to the default theme, making it easier to override 4523 // styles of the base-component 4524 if (theme.name == 'default') { 4525 var themeRuleRegex = /((?:(?:(?: |>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)+) )?)((?:(?:\w|\.|-)+)?)\.md-default-theme((?: |>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g; 4526 newRule = newRule.replace(themeRuleRegex, function(match, prefix, target, suffix) { 4527 return match + ', ' + prefix + target + suffix; 4528 }); 4529 } 4530 generatedRules.push(newRule); 4531 }); 4532 4533 return generatedRules; 4534 } 4535 4536 var rulesByType = {}; 4537 4538 // Generate our themes at run time given the state of THEMES and PALETTES 4539 function generateAllThemes($injector) { 4540 var head = document.head; 4541 var firstChild = head ? head.firstElementChild : null; 4542 var themeCss = $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : ''; 4543 4544 if ( !firstChild ) return; 4545 if (themeCss.length === 0) return; // no rules, so no point in running this expensive task 4546 4547 // Expose contrast colors for palettes to ensure that text is always readable 4548 angular.forEach(PALETTES, sanitizePalette); 4549 4550 // MD_THEME_CSS is a string generated by the build process that includes all the themable 4551 // components as templates 4552 4553 // Break the CSS into individual rules 4554 var rules = themeCss 4555 .split(/\}(?!(\}|'|"|;))/) 4556 .filter(function(rule) { return rule && rule.length; }) 4557 .map(function(rule) { return rule.trim() + '}'; }); 4558 4559 4560 var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g'); 4561 4562 THEME_COLOR_TYPES.forEach(function(type) { 4563 rulesByType[type] = ''; 4564 }); 4565 4566 4567 // Sort the rules based on type, allowing us to do color substitution on a per-type basis 4568 rules.forEach(function(rule) { 4569 var match = rule.match(ruleMatchRegex); 4570 // First: test that if the rule has '.md-accent', it goes into the accent set of rules 4571 for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) { 4572 if (rule.indexOf('.md-' + type) > -1) { 4573 return rulesByType[type] += rule; 4574 } 4575 } 4576 4577 // If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from 4578 // there 4579 for (i = 0; type = THEME_COLOR_TYPES[i]; i++) { 4580 if (rule.indexOf(type) > -1) { 4581 return rulesByType[type] += rule; 4582 } 4583 } 4584 4585 // Default to the primary array 4586 return rulesByType[DEFAULT_COLOR_TYPE] += rule; 4587 }); 4588 4589 // If themes are being generated on-demand, quit here. The user will later manually 4590 // call generateTheme to do this on a theme-by-theme basis. 4591 if (generateOnDemand) return; 4592 4593 angular.forEach(THEMES, function(theme) { 4594 if (!GENERATED[theme.name]) { 4595 generateTheme(theme.name, nonce); 4596 } 4597 }); 4598 4599 4600 // ************************* 4601 // Internal functions 4602 // ************************* 4603 4604 // The user specifies a 'default' contrast color as either light or dark, 4605 // then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light) 4606 function sanitizePalette(palette) { 4607 var defaultContrast = palette.contrastDefaultColor; 4608 var lightColors = palette.contrastLightColors || []; 4609 var strongLightColors = palette.contrastStrongLightColors || []; 4610 var darkColors = palette.contrastDarkColors || []; 4611 4612 // These colors are provided as space-separated lists 4613 if (typeof lightColors === 'string') lightColors = lightColors.split(' '); 4614 if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' '); 4615 if (typeof darkColors === 'string') darkColors = darkColors.split(' '); 4616 4617 // Cleanup after ourselves 4618 delete palette.contrastDefaultColor; 4619 delete palette.contrastLightColors; 4620 delete palette.contrastStrongLightColors; 4621 delete palette.contrastDarkColors; 4622 4623 // Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR } 4624 angular.forEach(palette, function(hueValue, hueName) { 4625 if (angular.isObject(hueValue)) return; // Already converted 4626 // Map everything to rgb colors 4627 var rgbValue = colorToRgbaArray(hueValue); 4628 if (!rgbValue) { 4629 throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected." 4630 .replace('%1', hueValue) 4631 .replace('%2', palette.name) 4632 .replace('%3', hueName)); 4633 } 4634 4635 palette[hueName] = { 4636 value: rgbValue, 4637 contrast: getContrastColor() 4638 }; 4639 function getContrastColor() { 4640 if (defaultContrast === 'light') { 4641 if (darkColors.indexOf(hueName) > -1) { 4642 return DARK_CONTRAST_COLOR; 4643 } else { 4644 return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR 4645 : LIGHT_CONTRAST_COLOR; 4646 } 4647 } else { 4648 if (lightColors.indexOf(hueName) > -1) { 4649 return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR 4650 : LIGHT_CONTRAST_COLOR; 4651 } else { 4652 return DARK_CONTRAST_COLOR; 4653 } 4654 } 4655 } 4656 }); 4657 } 4658 } 4659 generateAllThemes.$inject = ["$injector"]; 4660 4661 function generateTheme(name, nonce) { 4662 var theme = THEMES[name]; 4663 var head = document.head; 4664 var firstChild = head ? head.firstElementChild : null; 4665 4666 if (!GENERATED[name]) { 4667 // For each theme, use the color palettes specified for 4668 // `primary`, `warn` and `accent` to generate CSS rules. 4669 THEME_COLOR_TYPES.forEach(function(colorType) { 4670 var styleStrings = parseRules(theme, colorType, rulesByType[colorType]); 4671 while (styleStrings.length) { 4672 var styleContent = styleStrings.shift(); 4673 if (styleContent) { 4674 var style = document.createElement('style'); 4675 style.setAttribute('md-theme-style', ''); 4676 if (nonce) { 4677 style.setAttribute('nonce', nonce); 4678 } 4679 style.appendChild(document.createTextNode(styleContent)); 4680 head.insertBefore(style, firstChild); 4681 } 4682 } 4683 }); 4684 4685 4686 if (theme.colors.primary.name == theme.colors.accent.name) { 4687 console.warn('$mdThemingProvider: Using the same palette for primary and' + 4688 ' accent. This violates the material design spec.'); 4689 } 4690 4691 GENERATED[theme.name] = true; 4692 } 4693 4694 } 4695 4696 4697 function checkValidPalette(theme, colorType) { 4698 // If theme attempts to use a palette that doesnt exist, throw error 4699 if (!PALETTES[ (theme.colors[colorType] || {}).name ]) { 4700 throw new Error( 4701 "You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3" 4702 .replace('%1', theme.name) 4703 .replace('%2', colorType) 4704 .replace('%3', Object.keys(PALETTES).join(', ')) 4705 ); 4706 } 4707 } 4708 4709 function colorToRgbaArray(clr) { 4710 if (angular.isArray(clr) && clr.length == 3) return clr; 4711 if (/^rgb/.test(clr)) { 4712 return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) { 4713 return i == 3 ? parseFloat(value, 10) : parseInt(value, 10); 4714 }); 4715 } 4716 if (clr.charAt(0) == '#') clr = clr.substring(1); 4717 if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return; 4718 4719 var dig = clr.length / 3; 4720 var red = clr.substr(0, dig); 4721 var grn = clr.substr(dig, dig); 4722 var blu = clr.substr(dig * 2); 4723 if (dig === 1) { 4724 red += red; 4725 grn += grn; 4726 blu += blu; 4727 } 4728 return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)]; 4729 } 4730 4731 function rgba(rgbArray, opacity) { 4732 if ( !rgbArray ) return "rgb('0,0,0')"; 4733 4734 if (rgbArray.length == 4) { 4735 rgbArray = angular.copy(rgbArray); 4736 opacity ? rgbArray.pop() : opacity = rgbArray.pop(); 4737 } 4738 return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ? 4739 'rgba(' + rgbArray.join(',') + ',' + opacity + ')' : 4740 'rgb(' + rgbArray.join(',') + ')'; 4741 } 4742 4743 4744 })(); 4745 (function(){ 4746 "use strict"; 4747 4748 (function() { 4749 'use strict'; 4750 4751 /** 4752 * @ngdoc service 4753 * @name $mdButtonInkRipple 4754 * @module material.core 4755 * 4756 * @description 4757 * Provides ripple effects for md-button. See $mdInkRipple service for all possible configuration options. 4758 * 4759 * @param {object=} scope Scope within the current context 4760 * @param {object=} element The element the ripple effect should be applied to 4761 * @param {object=} options (Optional) Configuration options to override the default ripple configuration 4762 */ 4763 4764 angular.module('material.core') 4765 .factory('$mdButtonInkRipple', MdButtonInkRipple); 4766 4767 function MdButtonInkRipple($mdInkRipple) { 4768 return { 4769 attach: function attachRipple(scope, element, options) { 4770 options = angular.extend(optionsForElement(element), options); 4771 4772 return $mdInkRipple.attach(scope, element, options); 4773 } 4774 }; 4775 4776 function optionsForElement(element) { 4777 if (element.hasClass('md-icon-button')) { 4778 return { 4779 isMenuItem: element.hasClass('md-menu-item'), 4780 fitRipple: true, 4781 center: true 4782 }; 4783 } else { 4784 return { 4785 isMenuItem: element.hasClass('md-menu-item'), 4786 dimBackground: true 4787 } 4788 } 4789 }; 4790 } 4791 MdButtonInkRipple.$inject = ["$mdInkRipple"];; 4792 })(); 4793 4794 })(); 4795 (function(){ 4796 "use strict"; 4797 4798 (function() { 4799 'use strict'; 4800 4801 /** 4802 * @ngdoc service 4803 * @name $mdCheckboxInkRipple 4804 * @module material.core 4805 * 4806 * @description 4807 * Provides ripple effects for md-checkbox. See $mdInkRipple service for all possible configuration options. 4808 * 4809 * @param {object=} scope Scope within the current context 4810 * @param {object=} element The element the ripple effect should be applied to 4811 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration 4812 */ 4813 4814 angular.module('material.core') 4815 .factory('$mdCheckboxInkRipple', MdCheckboxInkRipple); 4816 4817 function MdCheckboxInkRipple($mdInkRipple) { 4818 return { 4819 attach: attach 4820 }; 4821 4822 function attach(scope, element, options) { 4823 return $mdInkRipple.attach(scope, element, angular.extend({ 4824 center: true, 4825 dimBackground: false, 4826 fitRipple: true 4827 }, options)); 4828 }; 4829 } 4830 MdCheckboxInkRipple.$inject = ["$mdInkRipple"];; 4831 })(); 4832 4833 })(); 4834 (function(){ 4835 "use strict"; 4836 4837 (function() { 4838 'use strict'; 4839 4840 /** 4841 * @ngdoc service 4842 * @name $mdListInkRipple 4843 * @module material.core 4844 * 4845 * @description 4846 * Provides ripple effects for md-list. See $mdInkRipple service for all possible configuration options. 4847 * 4848 * @param {object=} scope Scope within the current context 4849 * @param {object=} element The element the ripple effect should be applied to 4850 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration 4851 */ 4852 4853 angular.module('material.core') 4854 .factory('$mdListInkRipple', MdListInkRipple); 4855 4856 function MdListInkRipple($mdInkRipple) { 4857 return { 4858 attach: attach 4859 }; 4860 4861 function attach(scope, element, options) { 4862 return $mdInkRipple.attach(scope, element, angular.extend({ 4863 center: false, 4864 dimBackground: true, 4865 outline: false, 4866 rippleSize: 'full' 4867 }, options)); 4868 }; 4869 } 4870 MdListInkRipple.$inject = ["$mdInkRipple"];; 4871 })(); 4872 4873 })(); 4874 (function(){ 4875 "use strict"; 4876 4877 /** 4878 * @ngdoc module 4879 * @name material.core.ripple 4880 * @description 4881 * Ripple 4882 */ 4883 angular.module('material.core') 4884 .factory('$mdInkRipple', InkRippleService) 4885 .directive('mdInkRipple', InkRippleDirective) 4886 .directive('mdNoInk', attrNoDirective) 4887 .directive('mdNoBar', attrNoDirective) 4888 .directive('mdNoStretch', attrNoDirective); 4889 4890 var DURATION = 450; 4891 4892 /** 4893 * @ngdoc directive 4894 * @name mdInkRipple 4895 * @module material.core.ripple 4896 * 4897 * @description 4898 * The `md-ink-ripple` directive allows you to specify the ripple color or id a ripple is allowed. 4899 * 4900 * @param {string|boolean} md-ink-ripple A color string `#FF0000` or boolean (`false` or `0`) for preventing ripple 4901 * 4902 * @usage 4903 * ### String values 4904 * <hljs lang="html"> 4905 * <ANY md-ink-ripple="#FF0000"> 4906 * Ripples in red 4907 * </ANY> 4908 * 4909 * <ANY md-ink-ripple="false"> 4910 * Not rippling 4911 * </ANY> 4912 * </hljs> 4913 * 4914 * ### Interpolated values 4915 * <hljs lang="html"> 4916 * <ANY md-ink-ripple="{{ randomColor() }}"> 4917 * Ripples with the return value of 'randomColor' function 4918 * </ANY> 4919 * 4920 * <ANY md-ink-ripple="{{ canRipple() }}"> 4921 * Ripples if 'canRipple' function return value is not 'false' or '0' 4922 * </ANY> 4923 * </hljs> 4924 */ 4925 function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) { 4926 return { 4927 controller: angular.noop, 4928 link: function (scope, element, attr) { 4929 attr.hasOwnProperty('mdInkRippleCheckbox') 4930 ? $mdCheckboxInkRipple.attach(scope, element) 4931 : $mdButtonInkRipple.attach(scope, element); 4932 } 4933 }; 4934 } 4935 InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"]; 4936 4937 /** 4938 * @ngdoc service 4939 * @name $mdInkRipple 4940 * @module material.core.ripple 4941 * 4942 * @description 4943 * `$mdInkRipple` is a service for adding ripples to any element 4944 * 4945 * @usage 4946 * <hljs lang="js"> 4947 * app.factory('$myElementInkRipple', function($mdInkRipple) { 4948 * return { 4949 * attach: function (scope, element, options) { 4950 * return $mdInkRipple.attach(scope, element, angular.extend({ 4951 * center: false, 4952 * dimBackground: true 4953 * }, options)); 4954 * } 4955 * }; 4956 * }); 4957 * 4958 * app.controller('myController', function ($scope, $element, $myElementInkRipple) { 4959 * $scope.onClick = function (ev) { 4960 * $myElementInkRipple.attach($scope, angular.element(ev.target), { center: true }); 4961 * } 4962 * }); 4963 * </hljs> 4964 */ 4965 4966 /** 4967 * @ngdoc method 4968 * @name $mdInkRipple#attach 4969 * 4970 * @description 4971 * Attaching given scope, element and options to inkRipple controller 4972 * 4973 * @param {object=} scope Scope within the current context 4974 * @param {object=} element The element the ripple effect should be applied to 4975 * @param {object=} options (Optional) Configuration options to override the defaultRipple configuration 4976 * * `center` - Whether the ripple should start from the center of the container element 4977 * * `dimBackground` - Whether the background should be dimmed with the ripple color 4978 * * `colorElement` - The element the ripple should take its color from, defined by css property `color` 4979 * * `fitRipple` - Whether the ripple should fill the element 4980 */ 4981 function InkRippleService ($injector) { 4982 return { attach: attach }; 4983 function attach (scope, element, options) { 4984 if (element.controller('mdNoInk')) return angular.noop; 4985 return $injector.instantiate(InkRippleCtrl, { 4986 $scope: scope, 4987 $element: element, 4988 rippleOptions: options 4989 }); 4990 } 4991 } 4992 InkRippleService.$inject = ["$injector"]; 4993 4994 /** 4995 * Controller used by the ripple service in order to apply ripples 4996 * @ngInject 4997 */ 4998 function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil) { 4999 this.$window = $window; 5000 this.$timeout = $timeout; 5001 this.$mdUtil = $mdUtil; 5002 this.$scope = $scope; 5003 this.$element = $element; 5004 this.options = rippleOptions; 5005 this.mousedown = false; 5006 this.ripples = []; 5007 this.timeout = null; // Stores a reference to the most-recent ripple timeout 5008 this.lastRipple = null; 5009 5010 $mdUtil.valueOnUse(this, 'container', this.createContainer); 5011 5012 this.$element.addClass('md-ink-ripple'); 5013 5014 // attach method for unit tests 5015 ($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple); 5016 ($element.controller('mdInkRipple') || {}).setColor = angular.bind(this, this.color); 5017 5018 this.bindEvents(); 5019 } 5020 InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil"]; 5021 5022 5023 /** 5024 * Either remove or unlock any remaining ripples when the user mouses off of the element (either by 5025 * mouseup or mouseleave event) 5026 */ 5027 function autoCleanup (self, cleanupFn) { 5028 5029 if ( self.mousedown || self.lastRipple ) { 5030 self.mousedown = false; 5031 self.$mdUtil.nextTick( angular.bind(self, cleanupFn), false); 5032 } 5033 5034 } 5035 5036 5037 /** 5038 * Returns the color that the ripple should be (either based on CSS or hard-coded) 5039 * @returns {string} 5040 */ 5041 InkRippleCtrl.prototype.color = function (value) { 5042 var self = this; 5043 5044 // If assigning a color value, apply it to background and the ripple color 5045 if (angular.isDefined(value)) { 5046 self._color = self._parseColor(value); 5047 } 5048 5049 // If color lookup, use assigned, defined, or inherited 5050 return self._color || self._parseColor( self.inkRipple() ) || self._parseColor( getElementColor() ); 5051 5052 /** 5053 * Finds the color element and returns its text color for use as default ripple color 5054 * @returns {string} 5055 */ 5056 function getElementColor () { 5057 var items = self.options && self.options.colorElement ? self.options.colorElement : []; 5058 var elem = items.length ? items[ 0 ] : self.$element[ 0 ]; 5059 5060 return elem ? self.$window.getComputedStyle(elem).color : 'rgb(0,0,0)'; 5061 } 5062 }; 5063 5064 /** 5065 * Updating the ripple colors based on the current inkRipple value 5066 * or the element's computed style color 5067 */ 5068 InkRippleCtrl.prototype.calculateColor = function () { 5069 return this.color(); 5070 }; 5071 5072 5073 /** 5074 * Takes a string color and converts it to RGBA format 5075 * @param color {string} 5076 * @param [multiplier] {int} 5077 * @returns {string} 5078 */ 5079 5080 InkRippleCtrl.prototype._parseColor = function parseColor (color, multiplier) { 5081 multiplier = multiplier || 1; 5082 5083 if (!color) return; 5084 if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, (0.1 * multiplier).toString() + ')'); 5085 if (color.indexOf('rgb') === 0) return rgbToRGBA(color); 5086 if (color.indexOf('#') === 0) return hexToRGBA(color); 5087 5088 /** 5089 * Converts hex value to RGBA string 5090 * @param color {string} 5091 * @returns {string} 5092 */ 5093 function hexToRGBA (color) { 5094 var hex = color[ 0 ] === '#' ? color.substr(1) : color, 5095 dig = hex.length / 3, 5096 red = hex.substr(0, dig), 5097 green = hex.substr(dig, dig), 5098 blue = hex.substr(dig * 2); 5099 if (dig === 1) { 5100 red += red; 5101 green += green; 5102 blue += blue; 5103 } 5104 return 'rgba(' + parseInt(red, 16) + ',' + parseInt(green, 16) + ',' + parseInt(blue, 16) + ',0.1)'; 5105 } 5106 5107 /** 5108 * Converts an RGB color to RGBA 5109 * @param color {string} 5110 * @returns {string} 5111 */ 5112 function rgbToRGBA (color) { 5113 return color.replace(')', ', 0.1)').replace('(', 'a('); 5114 } 5115 5116 }; 5117 5118 /** 5119 * Binds events to the root element for 5120 */ 5121 InkRippleCtrl.prototype.bindEvents = function () { 5122 this.$element.on('mousedown', angular.bind(this, this.handleMousedown)); 5123 this.$element.on('mouseup touchend', angular.bind(this, this.handleMouseup)); 5124 this.$element.on('mouseleave', angular.bind(this, this.handleMouseup)); 5125 this.$element.on('touchmove', angular.bind(this, this.handleTouchmove)); 5126 }; 5127 5128 /** 5129 * Create a new ripple on every mousedown event from the root element 5130 * @param event {MouseEvent} 5131 */ 5132 InkRippleCtrl.prototype.handleMousedown = function (event) { 5133 if ( this.mousedown ) return; 5134 5135 // When jQuery is loaded, we have to get the original event 5136 if (event.hasOwnProperty('originalEvent')) event = event.originalEvent; 5137 this.mousedown = true; 5138 if (this.options.center) { 5139 this.createRipple(this.container.prop('clientWidth') / 2, this.container.prop('clientWidth') / 2); 5140 } else { 5141 5142 // We need to calculate the relative coordinates if the target is a sublayer of the ripple element 5143 if (event.srcElement !== this.$element[0]) { 5144 var layerRect = this.$element[0].getBoundingClientRect(); 5145 var layerX = event.clientX - layerRect.left; 5146 var layerY = event.clientY - layerRect.top; 5147 5148 this.createRipple(layerX, layerY); 5149 } else { 5150 this.createRipple(event.offsetX, event.offsetY); 5151 } 5152 } 5153 }; 5154 5155 /** 5156 * Either remove or unlock any remaining ripples when the user mouses off of the element (either by 5157 * mouseup, touchend or mouseleave event) 5158 */ 5159 InkRippleCtrl.prototype.handleMouseup = function () { 5160 autoCleanup(this, this.clearRipples); 5161 }; 5162 5163 /** 5164 * Either remove or unlock any remaining ripples when the user mouses off of the element (by 5165 * touchmove) 5166 */ 5167 InkRippleCtrl.prototype.handleTouchmove = function () { 5168 autoCleanup(this, this.deleteRipples); 5169 }; 5170 5171 /** 5172 * Cycles through all ripples and attempts to remove them. 5173 */ 5174 InkRippleCtrl.prototype.deleteRipples = function () { 5175 for (var i = 0; i < this.ripples.length; i++) { 5176 this.ripples[ i ].remove(); 5177 } 5178 }; 5179 5180 /** 5181 * Cycles through all ripples and attempts to remove them with fade. 5182 * Depending on logic within `fadeInComplete`, some removals will be postponed. 5183 */ 5184 InkRippleCtrl.prototype.clearRipples = function () { 5185 for (var i = 0; i < this.ripples.length; i++) { 5186 this.fadeInComplete(this.ripples[ i ]); 5187 } 5188 }; 5189 5190 /** 5191 * Creates the ripple container element 5192 * @returns {*} 5193 */ 5194 InkRippleCtrl.prototype.createContainer = function () { 5195 var container = angular.element('<div class="md-ripple-container"></div>'); 5196 this.$element.append(container); 5197 return container; 5198 }; 5199 5200 InkRippleCtrl.prototype.clearTimeout = function () { 5201 if (this.timeout) { 5202 this.$timeout.cancel(this.timeout); 5203 this.timeout = null; 5204 } 5205 }; 5206 5207 InkRippleCtrl.prototype.isRippleAllowed = function () { 5208 var element = this.$element[0]; 5209 do { 5210 if (!element.tagName || element.tagName === 'BODY') break; 5211 5212 if (element && angular.isFunction(element.hasAttribute)) { 5213 if (element.hasAttribute('disabled')) return false; 5214 if (this.inkRipple() === 'false' || this.inkRipple() === '0') return false; 5215 } 5216 5217 } while (element = element.parentNode); 5218 return true; 5219 }; 5220 5221 /** 5222 * The attribute `md-ink-ripple` may be a static or interpolated 5223 * color value OR a boolean indicator (used to disable ripples) 5224 */ 5225 InkRippleCtrl.prototype.inkRipple = function () { 5226 return this.$element.attr('md-ink-ripple'); 5227 }; 5228 5229 /** 5230 * Creates a new ripple and adds it to the container. Also tracks ripple in `this.ripples`. 5231 * @param left 5232 * @param top 5233 */ 5234 InkRippleCtrl.prototype.createRipple = function (left, top) { 5235 if (!this.isRippleAllowed()) return; 5236 5237 var ctrl = this; 5238 var ripple = angular.element('<div class="md-ripple"></div>'); 5239 var width = this.$element.prop('clientWidth'); 5240 var height = this.$element.prop('clientHeight'); 5241 var x = Math.max(Math.abs(width - left), left) * 2; 5242 var y = Math.max(Math.abs(height - top), top) * 2; 5243 var size = getSize(this.options.fitRipple, x, y); 5244 var color = this.calculateColor(); 5245 5246 ripple.css({ 5247 left: left + 'px', 5248 top: top + 'px', 5249 background: 'black', 5250 width: size + 'px', 5251 height: size + 'px', 5252 backgroundColor: rgbaToRGB(color), 5253 borderColor: rgbaToRGB(color) 5254 }); 5255 this.lastRipple = ripple; 5256 5257 // we only want one timeout to be running at a time 5258 this.clearTimeout(); 5259 this.timeout = this.$timeout(function () { 5260 ctrl.clearTimeout(); 5261 if (!ctrl.mousedown) ctrl.fadeInComplete(ripple); 5262 }, DURATION * 0.35, false); 5263 5264 if (this.options.dimBackground) this.container.css({ backgroundColor: color }); 5265 this.container.append(ripple); 5266 this.ripples.push(ripple); 5267 ripple.addClass('md-ripple-placed'); 5268 5269 this.$mdUtil.nextTick(function () { 5270 5271 ripple.addClass('md-ripple-scaled md-ripple-active'); 5272 ctrl.$timeout(function () { 5273 ctrl.clearRipples(); 5274 }, DURATION, false); 5275 5276 }, false); 5277 5278 function rgbaToRGB (color) { 5279 return color 5280 ? color.replace('rgba', 'rgb').replace(/,[^\),]+\)/, ')') 5281 : 'rgb(0,0,0)'; 5282 } 5283 5284 function getSize (fit, x, y) { 5285 return fit 5286 ? Math.max(x, y) 5287 : Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); 5288 } 5289 }; 5290 5291 5292 5293 /** 5294 * After fadeIn finishes, either kicks off the fade-out animation or queues the element for removal on mouseup 5295 * @param ripple 5296 */ 5297 InkRippleCtrl.prototype.fadeInComplete = function (ripple) { 5298 if (this.lastRipple === ripple) { 5299 if (!this.timeout && !this.mousedown) { 5300 this.removeRipple(ripple); 5301 } 5302 } else { 5303 this.removeRipple(ripple); 5304 } 5305 }; 5306 5307 /** 5308 * Kicks off the animation for removing a ripple 5309 * @param ripple {Element} 5310 */ 5311 InkRippleCtrl.prototype.removeRipple = function (ripple) { 5312 var ctrl = this; 5313 var index = this.ripples.indexOf(ripple); 5314 if (index < 0) return; 5315 this.ripples.splice(this.ripples.indexOf(ripple), 1); 5316 ripple.removeClass('md-ripple-active'); 5317 if (this.ripples.length === 0) this.container.css({ backgroundColor: '' }); 5318 // use a 2-second timeout in order to allow for the animation to finish 5319 // we don't actually care how long the animation takes 5320 this.$timeout(function () { 5321 ctrl.fadeOutComplete(ripple); 5322 }, DURATION, false); 5323 }; 5324 5325 /** 5326 * Removes the provided ripple from the DOM 5327 * @param ripple 5328 */ 5329 InkRippleCtrl.prototype.fadeOutComplete = function (ripple) { 5330 ripple.remove(); 5331 this.lastRipple = null; 5332 }; 5333 5334 /** 5335 * Used to create an empty directive. This is used to track flag-directives whose children may have 5336 * functionality based on them. 5337 * 5338 * Example: `md-no-ink` will potentially be used by all child directives. 5339 */ 5340 function attrNoDirective () { 5341 return { controller: angular.noop }; 5342 } 5343 5344 })(); 5345 (function(){ 5346 "use strict"; 5347 5348 (function() { 5349 'use strict'; 5350 5351 /** 5352 * @ngdoc service 5353 * @name $mdTabInkRipple 5354 * @module material.core 5355 * 5356 * @description 5357 * Provides ripple effects for md-tabs. See $mdInkRipple service for all possible configuration options. 5358 * 5359 * @param {object=} scope Scope within the current context 5360 * @param {object=} element The element the ripple effect should be applied to 5361 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration 5362 */ 5363 5364 angular.module('material.core') 5365 .factory('$mdTabInkRipple', MdTabInkRipple); 5366 5367 function MdTabInkRipple($mdInkRipple) { 5368 return { 5369 attach: attach 5370 }; 5371 5372 function attach(scope, element, options) { 5373 return $mdInkRipple.attach(scope, element, angular.extend({ 5374 center: false, 5375 dimBackground: true, 5376 outline: false, 5377 rippleSize: 'full' 5378 }, options)); 5379 }; 5380 } 5381 MdTabInkRipple.$inject = ["$mdInkRipple"];; 5382 })(); 5383 5384 })(); 5385 (function(){ 5386 "use strict"; 5387 5388 // Polyfill angular < 1.4 (provide $animateCss) 5389 angular 5390 .module('material.core') 5391 .factory('$$mdAnimate', ["$q", "$timeout", "$mdConstant", "$animateCss", function($q, $timeout, $mdConstant, $animateCss){ 5392 5393 // Since $$mdAnimate is injected into $mdUtil... use a wrapper function 5394 // to subsequently inject $mdUtil as an argument to the AnimateDomUtils 5395 5396 return function($mdUtil) { 5397 return AnimateDomUtils( $mdUtil, $q, $timeout, $mdConstant, $animateCss); 5398 }; 5399 }]); 5400 5401 /** 5402 * Factory function that requires special injections 5403 */ 5404 function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { 5405 var self; 5406 return self = { 5407 /** 5408 * 5409 */ 5410 translate3d : function( target, from, to, options ) { 5411 return $animateCss(target,{ 5412 from:from, 5413 to:to, 5414 addClass:options.transitionInClass 5415 }) 5416 .start() 5417 .then(function(){ 5418 // Resolve with reverser function... 5419 return reverseTranslate; 5420 }); 5421 5422 /** 5423 * Specific reversal of the request translate animation above... 5424 */ 5425 function reverseTranslate (newFrom) { 5426 return $animateCss(target, { 5427 to: newFrom || from, 5428 addClass: options.transitionOutClass, 5429 removeClass: options.transitionInClass 5430 }).start(); 5431 5432 } 5433 }, 5434 5435 /** 5436 * Listen for transitionEnd event (with optional timeout) 5437 * Announce completion or failure via promise handlers 5438 */ 5439 waitTransitionEnd: function (element, opts) { 5440 var TIMEOUT = 3000; // fallback is 3 secs 5441 5442 return $q(function(resolve, reject){ 5443 opts = opts || { }; 5444 5445 var timer = $timeout(finished, opts.timeout || TIMEOUT); 5446 element.on($mdConstant.CSS.TRANSITIONEND, finished); 5447 5448 /** 5449 * Upon timeout or transitionEnd, reject or resolve (respectively) this promise. 5450 * NOTE: Make sure this transitionEnd didn't bubble up from a child 5451 */ 5452 function finished(ev) { 5453 if ( ev && ev.target !== element[0]) return; 5454 5455 if ( ev ) $timeout.cancel(timer); 5456 element.off($mdConstant.CSS.TRANSITIONEND, finished); 5457 5458 // Never reject since ngAnimate may cause timeouts due missed transitionEnd events 5459 resolve(); 5460 5461 } 5462 5463 }); 5464 }, 5465 5466 /** 5467 * Calculate the zoom transform from dialog to origin. 5468 * 5469 * We use this to set the dialog position immediately; 5470 * then the md-transition-in actually translates back to 5471 * `translate3d(0,0,0) scale(1.0)`... 5472 * 5473 * NOTE: all values are rounded to the nearest integer 5474 */ 5475 calculateZoomToOrigin: function (element, originator) { 5476 var origin = originator.element; 5477 var bounds = originator.bounds; 5478 5479 var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )"; 5480 var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate); 5481 var zoomStyle = buildZoom({centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5}); 5482 5483 if (origin || bounds) { 5484 var originBnds = origin ? self.clientRect(origin) || currentBounds() : self.copyRect(bounds); 5485 var dialogRect = self.copyRect(element[0].getBoundingClientRect()); 5486 var dialogCenterPt = self.centerPointFor(dialogRect); 5487 var originCenterPt = self.centerPointFor(originBnds); 5488 5489 // Build the transform to zoom from the dialog center to the origin center 5490 5491 zoomStyle = buildZoom({ 5492 centerX: originCenterPt.x - dialogCenterPt.x, 5493 centerY: originCenterPt.y - dialogCenterPt.y, 5494 scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width))/100, 5495 scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height))/100 5496 }); 5497 } 5498 5499 return zoomStyle; 5500 5501 /** 5502 * This is a fallback if the origin information is no longer valid, then the 5503 * origin bounds simply becomes the current bounds for the dialogContainer's parent 5504 */ 5505 function currentBounds() { 5506 var cntr = element ? element.parent() : null; 5507 var parent = cntr ? cntr.parent() : null; 5508 5509 return parent ? self.clientRect(parent) : null; 5510 } 5511 }, 5512 5513 /** 5514 * Enhance raw values to represent valid css stylings... 5515 */ 5516 toCss : function( raw ) { 5517 var css = { }; 5518 var lookups = 'left top right bottom width height x y min-width min-height max-width max-height'; 5519 5520 angular.forEach(raw, function(value,key) { 5521 if ( angular.isUndefined(value) ) return; 5522 5523 if ( lookups.indexOf(key) >= 0 ) { 5524 css[key] = value + 'px'; 5525 } else { 5526 switch (key) { 5527 case 'transition': 5528 convertToVendor(key, $mdConstant.CSS.TRANSITION, value); 5529 break; 5530 case 'transform': 5531 convertToVendor(key, $mdConstant.CSS.TRANSFORM, value); 5532 break; 5533 case 'transformOrigin': 5534 convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value); 5535 break; 5536 } 5537 } 5538 }); 5539 5540 return css; 5541 5542 function convertToVendor(key, vendor, value) { 5543 angular.forEach(vendor.split(' '), function (key) { 5544 css[key] = value; 5545 }); 5546 } 5547 }, 5548 5549 /** 5550 * Convert the translate CSS value to key/value pair(s). 5551 */ 5552 toTransformCss: function (transform, addTransition, transition) { 5553 var css = {}; 5554 angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) { 5555 css[key] = transform; 5556 }); 5557 5558 if (addTransition) { 5559 transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important"; 5560 css['transition'] = transition; 5561 } 5562 5563 return css; 5564 }, 5565 5566 /** 5567 * Clone the Rect and calculate the height/width if needed 5568 */ 5569 copyRect: function (source, destination) { 5570 if (!source) return null; 5571 5572 destination = destination || {}; 5573 5574 angular.forEach('left top right bottom width height'.split(' '), function (key) { 5575 destination[key] = Math.round(source[key]) 5576 }); 5577 5578 destination.width = destination.width || (destination.right - destination.left); 5579 destination.height = destination.height || (destination.bottom - destination.top); 5580 5581 return destination; 5582 }, 5583 5584 /** 5585 * Calculate ClientRect of element; return null if hidden or zero size 5586 */ 5587 clientRect: function (element) { 5588 var bounds = angular.element(element)[0].getBoundingClientRect(); 5589 var isPositiveSizeClientRect = function (rect) { 5590 return rect && (rect.width > 0) && (rect.height > 0); 5591 }; 5592 5593 // If the event origin element has zero size, it has probably been hidden. 5594 return isPositiveSizeClientRect(bounds) ? self.copyRect(bounds) : null; 5595 }, 5596 5597 /** 5598 * Calculate 'rounded' center point of Rect 5599 */ 5600 centerPointFor: function (targetRect) { 5601 return targetRect ? { 5602 x: Math.round(targetRect.left + (targetRect.width / 2)), 5603 y: Math.round(targetRect.top + (targetRect.height / 2)) 5604 } : { x : 0, y : 0 }; 5605 } 5606 5607 }; 5608 }; 5609 5610 5611 })(); 5612 (function(){ 5613 "use strict"; 5614 5615 "use strict"; 5616 5617 if (angular.version.minor >= 4) { 5618 angular.module('material.core.animate', []); 5619 } else { 5620 (function() { 5621 5622 var forEach = angular.forEach; 5623 5624 var WEBKIT = angular.isDefined(document.documentElement.style.WebkitAppearance); 5625 var TRANSITION_PROP = WEBKIT ? 'WebkitTransition' : 'transition'; 5626 var ANIMATION_PROP = WEBKIT ? 'WebkitAnimation' : 'animation'; 5627 var PREFIX = WEBKIT ? '-webkit-' : ''; 5628 5629 var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend'; 5630 var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend'; 5631 5632 var $$ForceReflowFactory = ['$document', function($document) { 5633 return function() { 5634 return $document[0].body.clientWidth + 1; 5635 } 5636 }]; 5637 5638 var $$rAFMutexFactory = ['$$rAF', function($$rAF) { 5639 return function() { 5640 var passed = false; 5641 $$rAF(function() { 5642 passed = true; 5643 }); 5644 return function(fn) { 5645 passed ? fn() : $$rAF(fn); 5646 }; 5647 }; 5648 }]; 5649 5650 var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) { 5651 var INITIAL_STATE = 0; 5652 var DONE_PENDING_STATE = 1; 5653 var DONE_COMPLETE_STATE = 2; 5654 5655 function AnimateRunner(host) { 5656 this.setHost(host); 5657 5658 this._doneCallbacks = []; 5659 this._runInAnimationFrame = $$rAFMutex(); 5660 this._state = 0; 5661 } 5662 5663 AnimateRunner.prototype = { 5664 setHost: function(host) { 5665 this.host = host || {}; 5666 }, 5667 5668 done: function(fn) { 5669 if (this._state === DONE_COMPLETE_STATE) { 5670 fn(); 5671 } else { 5672 this._doneCallbacks.push(fn); 5673 } 5674 }, 5675 5676 progress: angular.noop, 5677 5678 getPromise: function() { 5679 if (!this.promise) { 5680 var self = this; 5681 this.promise = $q(function(resolve, reject) { 5682 self.done(function(status) { 5683 status === false ? reject() : resolve(); 5684 }); 5685 }); 5686 } 5687 return this.promise; 5688 }, 5689 5690 then: function(resolveHandler, rejectHandler) { 5691 return this.getPromise().then(resolveHandler, rejectHandler); 5692 }, 5693 5694 'catch': function(handler) { 5695 return this.getPromise()['catch'](handler); 5696 }, 5697 5698 'finally': function(handler) { 5699 return this.getPromise()['finally'](handler); 5700 }, 5701 5702 pause: function() { 5703 if (this.host.pause) { 5704 this.host.pause(); 5705 } 5706 }, 5707 5708 resume: function() { 5709 if (this.host.resume) { 5710 this.host.resume(); 5711 } 5712 }, 5713 5714 end: function() { 5715 if (this.host.end) { 5716 this.host.end(); 5717 } 5718 this._resolve(true); 5719 }, 5720 5721 cancel: function() { 5722 if (this.host.cancel) { 5723 this.host.cancel(); 5724 } 5725 this._resolve(false); 5726 }, 5727 5728 complete: function(response) { 5729 var self = this; 5730 if (self._state === INITIAL_STATE) { 5731 self._state = DONE_PENDING_STATE; 5732 self._runInAnimationFrame(function() { 5733 self._resolve(response); 5734 }); 5735 } 5736 }, 5737 5738 _resolve: function(response) { 5739 if (this._state !== DONE_COMPLETE_STATE) { 5740 forEach(this._doneCallbacks, function(fn) { 5741 fn(response); 5742 }); 5743 this._doneCallbacks.length = 0; 5744 this._state = DONE_COMPLETE_STATE; 5745 } 5746 } 5747 }; 5748 5749 return AnimateRunner; 5750 }]; 5751 5752 angular 5753 .module('material.core.animate', []) 5754 .factory('$$forceReflow', $$ForceReflowFactory) 5755 .factory('$$AnimateRunner', $$AnimateRunnerFactory) 5756 .factory('$$rAFMutex', $$rAFMutexFactory) 5757 .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout', 5758 function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout) { 5759 5760 function init(element, options) { 5761 5762 var temporaryStyles = []; 5763 var node = getDomNode(element); 5764 5765 if (options.transitionStyle) { 5766 temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]); 5767 } 5768 5769 if (options.keyframeStyle) { 5770 temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]); 5771 } 5772 5773 if (options.delay) { 5774 temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']); 5775 } 5776 5777 if (options.duration) { 5778 temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']); 5779 } 5780 5781 var hasCompleteStyles = options.keyframeStyle || 5782 (options.to && (options.duration > 0 || options.transitionStyle)); 5783 var hasCompleteClasses = !!options.addClass || !!options.removeClass; 5784 var hasCompleteAnimation = hasCompleteStyles || hasCompleteClasses; 5785 5786 blockTransition(element, true); 5787 applyAnimationFromStyles(element, options); 5788 5789 var animationClosed = false; 5790 var events, eventFn; 5791 5792 return { 5793 close: $window.close, 5794 start: function() { 5795 var runner = new $$AnimateRunner(); 5796 waitUntilQuiet(function() { 5797 blockTransition(element, false); 5798 if (!hasCompleteAnimation) { 5799 return close(); 5800 } 5801 5802 forEach(temporaryStyles, function(entry) { 5803 var key = entry[0]; 5804 var value = entry[1]; 5805 node.style[camelCase(key)] = value; 5806 }); 5807 5808 applyClasses(element, options); 5809 5810 var timings = computeTimings(element); 5811 if (timings.duration === 0) { 5812 return close(); 5813 } 5814 5815 var moreStyles = []; 5816 5817 if (options.easing) { 5818 if (timings.transitionDuration) { 5819 moreStyles.push([PREFIX + 'transition-timing-function', options.easing]); 5820 } 5821 if (timings.animationDuration) { 5822 moreStyles.push([PREFIX + 'animation-timing-function', options.easing]); 5823 } 5824 } 5825 5826 if (options.delay && timings.animationDelay) { 5827 moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']); 5828 } 5829 5830 if (options.duration && timings.animationDuration) { 5831 moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']); 5832 } 5833 5834 forEach(moreStyles, function(entry) { 5835 var key = entry[0]; 5836 var value = entry[1]; 5837 node.style[camelCase(key)] = value; 5838 temporaryStyles.push(entry); 5839 }); 5840 5841 var maxDelay = timings.delay; 5842 var maxDelayTime = maxDelay * 1000; 5843 var maxDuration = timings.duration; 5844 var maxDurationTime = maxDuration * 1000; 5845 var startTime = Date.now(); 5846 5847 events = []; 5848 if (timings.transitionDuration) { 5849 events.push(TRANSITION_EVENTS); 5850 } 5851 if (timings.animationDuration) { 5852 events.push(ANIMATION_EVENTS); 5853 } 5854 events = events.join(' '); 5855 eventFn = function(event) { 5856 event.stopPropagation(); 5857 var ev = event.originalEvent || event; 5858 var timeStamp = ev.timeStamp || Date.now(); 5859 var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3)); 5860 if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { 5861 close(); 5862 } 5863 }; 5864 element.on(events, eventFn); 5865 5866 applyAnimationToStyles(element, options); 5867 5868 $timeout(close, maxDelayTime + maxDurationTime * 1.5, false); 5869 }); 5870 5871 return runner; 5872 5873 function close() { 5874 if (animationClosed) return; 5875 animationClosed = true; 5876 5877 if (events && eventFn) { 5878 element.off(events, eventFn); 5879 } 5880 applyClasses(element, options); 5881 applyAnimationStyles(element, options); 5882 forEach(temporaryStyles, function(entry) { 5883 node.style[camelCase(entry[0])] = ''; 5884 }); 5885 runner.complete(true); 5886 return runner; 5887 } 5888 } 5889 } 5890 } 5891 5892 function applyClasses(element, options) { 5893 if (options.addClass) { 5894 $$jqLite.addClass(element, options.addClass); 5895 options.addClass = null; 5896 } 5897 if (options.removeClass) { 5898 $$jqLite.removeClass(element, options.removeClass); 5899 options.removeClass = null; 5900 } 5901 } 5902 5903 function computeTimings(element) { 5904 var node = getDomNode(element); 5905 var cs = $window.getComputedStyle(node) 5906 var tdr = parseMaxTime(cs[prop('transitionDuration')]); 5907 var adr = parseMaxTime(cs[prop('animationDuration')]); 5908 var tdy = parseMaxTime(cs[prop('transitionDelay')]); 5909 var ady = parseMaxTime(cs[prop('animationDelay')]); 5910 5911 adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1); 5912 var duration = Math.max(adr, tdr); 5913 var delay = Math.max(ady, tdy); 5914 5915 return { 5916 duration: duration, 5917 delay: delay, 5918 animationDuration: adr, 5919 transitionDuration: tdr, 5920 animationDelay: ady, 5921 transitionDelay: tdy 5922 }; 5923 5924 function prop(key) { 5925 return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1) 5926 : key; 5927 } 5928 } 5929 5930 function parseMaxTime(str) { 5931 var maxValue = 0; 5932 var values = (str || "").split(/\s*,\s*/); 5933 forEach(values, function(value) { 5934 // it's always safe to consider only second values and omit `ms` values since 5935 // getComputedStyle will always handle the conversion for us 5936 if (value.charAt(value.length - 1) == 's') { 5937 value = value.substring(0, value.length - 1); 5938 } 5939 value = parseFloat(value) || 0; 5940 maxValue = maxValue ? Math.max(value, maxValue) : value; 5941 }); 5942 return maxValue; 5943 } 5944 5945 var cancelLastRAFRequest; 5946 var rafWaitQueue = []; 5947 function waitUntilQuiet(callback) { 5948 if (cancelLastRAFRequest) { 5949 cancelLastRAFRequest(); //cancels the request 5950 } 5951 rafWaitQueue.push(callback); 5952 cancelLastRAFRequest = $$rAF(function() { 5953 cancelLastRAFRequest = null; 5954 5955 // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. 5956 // PLEASE EXAMINE THE `$$forceReflow` service to understand why. 5957 var pageWidth = $$forceReflow(); 5958 5959 // we use a for loop to ensure that if the queue is changed 5960 // during this looping then it will consider new requests 5961 for (var i = 0; i < rafWaitQueue.length; i++) { 5962 rafWaitQueue[i](pageWidth); 5963 } 5964 rafWaitQueue.length = 0; 5965 }); 5966 } 5967 5968 function applyAnimationStyles(element, options) { 5969 applyAnimationFromStyles(element, options); 5970 applyAnimationToStyles(element, options); 5971 } 5972 5973 function applyAnimationFromStyles(element, options) { 5974 if (options.from) { 5975 element.css(options.from); 5976 options.from = null; 5977 } 5978 } 5979 5980 function applyAnimationToStyles(element, options) { 5981 if (options.to) { 5982 element.css(options.to); 5983 options.to = null; 5984 } 5985 } 5986 5987 function getDomNode(element) { 5988 for (var i = 0; i < element.length; i++) { 5989 if (element[i].nodeType === 1) return element[i]; 5990 } 5991 } 5992 5993 function blockTransition(element, bool) { 5994 var node = getDomNode(element); 5995 var key = camelCase(PREFIX + 'transition-delay'); 5996 node.style[key] = bool ? '-9999s' : ''; 5997 } 5998 5999 return init; 6000 }]); 6001 6002 /** 6003 * Older browsers [FF31] expect camelCase 6004 * property keys. 6005 * e.g. 6006 * animation-duration --> animationDuration 6007 */ 6008 function camelCase(str) { 6009 return str.replace(/-[a-z]/g, function(str) { 6010 return str.charAt(1).toUpperCase(); 6011 }); 6012 } 6013 6014 })(); 6015 6016 } 6017 6018 })(); 6019 (function(){ 6020 "use strict"; 6021 6022 /** 6023 * @ngdoc module 6024 * @name material.components.autocomplete 6025 */ 6026 /* 6027 * @see js folder for autocomplete implementation 6028 */ 6029 angular.module('material.components.autocomplete', [ 6030 'material.core', 6031 'material.components.icon', 6032 'material.components.virtualRepeat' 6033 ]); 6034 6035 })(); 6036 (function(){ 6037 "use strict"; 6038 6039 /* 6040 * @ngdoc module 6041 * @name material.components.backdrop 6042 * @description Backdrop 6043 */ 6044 6045 /** 6046 * @ngdoc directive 6047 * @name mdBackdrop 6048 * @module material.components.backdrop 6049 * 6050 * @restrict E 6051 * 6052 * @description 6053 * `<md-backdrop>` is a backdrop element used by other components, such as dialog and bottom sheet. 6054 * Apply class `opaque` to make the backdrop use the theme backdrop color. 6055 * 6056 */ 6057 6058 angular 6059 .module('material.components.backdrop', ['material.core']) 6060 .directive('mdBackdrop', ["$mdTheming", "$animate", "$rootElement", "$window", "$log", "$$rAF", "$document", function BackdropDirective($mdTheming, $animate, $rootElement, $window, $log, $$rAF, $document) { 6061 var ERROR_CSS_POSITION = "<md-backdrop> may not work properly in a scrolled, static-positioned parent container."; 6062 6063 return { 6064 restrict: 'E', 6065 link: postLink 6066 }; 6067 6068 function postLink(scope, element, attrs) { 6069 6070 // If body scrolling has been disabled using mdUtil.disableBodyScroll(), 6071 // adjust the 'backdrop' height to account for the fixed 'body' top offset 6072 var body = $window.getComputedStyle($document[0].body); 6073 if (body.position == 'fixed') { 6074 var hViewport = parseInt(body.height, 10) + Math.abs(parseInt(body.top, 10)); 6075 element.css({ 6076 height: hViewport + 'px' 6077 }); 6078 } 6079 6080 // backdrop may be outside the $rootElement, tell ngAnimate to animate regardless 6081 if ($animate.pin) $animate.pin(element, $rootElement); 6082 6083 $$rAF(function () { 6084 6085 // Often $animate.enter() is used to append the backDrop element 6086 // so let's wait until $animate is done... 6087 var parent = element.parent()[0]; 6088 if (parent) { 6089 6090 if ( parent.nodeName == 'BODY' ) { 6091 element.css({position : 'fixed'}); 6092 } 6093 6094 var styles = $window.getComputedStyle(parent); 6095 if (styles.position == 'static') { 6096 // backdrop uses position:absolute and will not work properly with parent position:static (default) 6097 $log.warn(ERROR_CSS_POSITION); 6098 } 6099 } 6100 6101 // Only inherit the parent if the backdrop has a parent. 6102 if (element.parent().length) { 6103 $mdTheming.inherit(element, element.parent()); 6104 } 6105 }); 6106 6107 } 6108 6109 }]); 6110 6111 })(); 6112 (function(){ 6113 "use strict"; 6114 6115 /** 6116 * @ngdoc module 6117 * @name material.components.button 6118 * @description 6119 * 6120 * Button 6121 */ 6122 angular 6123 .module('material.components.button', [ 'material.core' ]) 6124 .directive('mdButton', MdButtonDirective); 6125 6126 /** 6127 * @ngdoc directive 6128 * @name mdButton 6129 * @module material.components.button 6130 * 6131 * @restrict E 6132 * 6133 * @description 6134 * `<md-button>` is a button directive with optional ink ripples (default enabled). 6135 * 6136 * If you supply a `href` or `ng-href` attribute, it will become an `<a>` element. Otherwise, it will 6137 * become a `<button>` element. As per the [Material Design specifications](http://www.google.com/design/spec/style/color.html#color-ui-color-application) 6138 * the FAB button background is filled with the accent color [by default]. The primary color palette may be used with 6139 * the `md-primary` class. 6140 * 6141 * @param {boolean=} md-no-ink If present, disable ripple ink effects. 6142 * @param {expression=} ng-disabled En/Disable based on the expression 6143 * @param {string=} md-ripple-size Overrides the default ripple size logic. Options: `full`, `partial`, `auto` 6144 * @param {string=} aria-label Adds alternative text to button for accessibility, useful for icon buttons. 6145 * If no default text is found, a warning will be logged. 6146 * 6147 * @usage 6148 * 6149 * Regular buttons: 6150 * 6151 * <hljs lang="html"> 6152 * <md-button> Flat Button </md-button> 6153 * <md-button href="http://google.com"> Flat link </md-button> 6154 * <md-button class="md-raised"> Raised Button </md-button> 6155 * <md-button ng-disabled="true"> Disabled Button </md-button> 6156 * <md-button> 6157 * <md-icon md-svg-src="your/icon.svg"></md-icon> 6158 * Register Now 6159 * </md-button> 6160 * </hljs> 6161 * 6162 * FAB buttons: 6163 * 6164 * <hljs lang="html"> 6165 * <md-button class="md-fab" aria-label="FAB"> 6166 * <md-icon md-svg-src="your/icon.svg"></md-icon> 6167 * </md-button> 6168 * <!-- mini-FAB --> 6169 * <md-button class="md-fab md-mini" aria-label="Mini FAB"> 6170 * <md-icon md-svg-src="your/icon.svg"></md-icon> 6171 * </md-button> 6172 * <!-- Button with SVG Icon --> 6173 * <md-button class="md-icon-button" aria-label="Custom Icon Button"> 6174 * <md-icon md-svg-icon="path/to/your.svg"></md-icon> 6175 * </md-button> 6176 * </hljs> 6177 */ 6178 function MdButtonDirective($mdButtonInkRipple, $mdTheming, $mdAria, $timeout) { 6179 6180 return { 6181 restrict: 'EA', 6182 replace: true, 6183 transclude: true, 6184 template: getTemplate, 6185 link: postLink 6186 }; 6187 6188 function isAnchor(attr) { 6189 return angular.isDefined(attr.href) || angular.isDefined(attr.ngHref) || angular.isDefined(attr.ngLink) || angular.isDefined(attr.uiSref); 6190 } 6191 6192 function getTemplate(element, attr) { 6193 if (isAnchor(attr)) { 6194 return '<a class="md-button" ng-transclude></a>'; 6195 } else { 6196 //If buttons don't have type="button", they will submit forms automatically. 6197 var btnType = (typeof attr.type === 'undefined') ? 'button' : attr.type; 6198 return '<button class="md-button" type="' + btnType + '" ng-transclude></button>'; 6199 } 6200 } 6201 6202 function postLink(scope, element, attr) { 6203 $mdTheming(element); 6204 $mdButtonInkRipple.attach(scope, element); 6205 6206 // Use async expect to support possible bindings in the button label 6207 $mdAria.expectWithText(element, 'aria-label'); 6208 6209 // For anchor elements, we have to set tabindex manually when the 6210 // element is disabled 6211 if (isAnchor(attr) && angular.isDefined(attr.ngDisabled) ) { 6212 scope.$watch(attr.ngDisabled, function(isDisabled) { 6213 element.attr('tabindex', isDisabled ? -1 : 0); 6214 }); 6215 } 6216 6217 // disabling click event when disabled is true 6218 element.on('click', function(e){ 6219 if (attr.disabled === true) { 6220 e.preventDefault(); 6221 e.stopImmediatePropagation(); 6222 } 6223 }); 6224 6225 // restrict focus styles to the keyboard 6226 scope.mouseActive = false; 6227 element.on('mousedown', function() { 6228 scope.mouseActive = true; 6229 $timeout(function(){ 6230 scope.mouseActive = false; 6231 }, 100); 6232 }) 6233 .on('focus', function() { 6234 if (scope.mouseActive === false) { 6235 element.addClass('md-focused'); 6236 } 6237 }) 6238 .on('blur', function(ev) { 6239 element.removeClass('md-focused'); 6240 }); 6241 } 6242 6243 } 6244 MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$timeout"]; 6245 6246 })(); 6247 (function(){ 6248 "use strict"; 6249 6250 /** 6251 * @ngdoc module 6252 * @name material.components.bottomSheet 6253 * @description 6254 * BottomSheet 6255 */ 6256 angular 6257 .module('material.components.bottomSheet', [ 6258 'material.core', 6259 'material.components.backdrop' 6260 ]) 6261 .directive('mdBottomSheet', MdBottomSheetDirective) 6262 .provider('$mdBottomSheet', MdBottomSheetProvider); 6263 6264 /* @ngInject */ 6265 function MdBottomSheetDirective($mdBottomSheet) { 6266 return { 6267 restrict: 'E', 6268 link : function postLink(scope, element, attr) { 6269 // When navigation force destroys an interimElement, then 6270 // listen and $destroy() that interim instance... 6271 scope.$on('$destroy', function() { 6272 $mdBottomSheet.destroy(); 6273 }); 6274 } 6275 }; 6276 } 6277 MdBottomSheetDirective.$inject = ["$mdBottomSheet"]; 6278 6279 6280 /** 6281 * @ngdoc service 6282 * @name $mdBottomSheet 6283 * @module material.components.bottomSheet 6284 * 6285 * @description 6286 * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API. 6287 * 6288 * ## Restrictions 6289 * 6290 * - The bottom sheet's template must have an outer `<md-bottom-sheet>` element. 6291 * - Add the `md-grid` class to the bottom sheet for a grid layout. 6292 * - Add the `md-list` class to the bottom sheet for a list layout. 6293 * 6294 * @usage 6295 * <hljs lang="html"> 6296 * <div ng-controller="MyController"> 6297 * <md-button ng-click="openBottomSheet()"> 6298 * Open a Bottom Sheet! 6299 * </md-button> 6300 * </div> 6301 * </hljs> 6302 * <hljs lang="js"> 6303 * var app = angular.module('app', ['ngMaterial']); 6304 * app.controller('MyController', function($scope, $mdBottomSheet) { 6305 * $scope.openBottomSheet = function() { 6306 * $mdBottomSheet.show({ 6307 * template: '<md-bottom-sheet>Hello!</md-bottom-sheet>' 6308 * }); 6309 * }; 6310 * }); 6311 * </hljs> 6312 */ 6313 6314 /** 6315 * @ngdoc method 6316 * @name $mdBottomSheet#show 6317 * 6318 * @description 6319 * Show a bottom sheet with the specified options. 6320 * 6321 * @param {object} options An options object, with the following properties: 6322 * 6323 * - `templateUrl` - `{string=}`: The url of an html template file that will 6324 * be used as the content of the bottom sheet. Restrictions: the template must 6325 * have an outer `md-bottom-sheet` element. 6326 * - `template` - `{string=}`: Same as templateUrl, except this is an actual 6327 * template string. 6328 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope. 6329 * This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true. 6330 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false 6331 * - `controller` - `{string=}`: The controller to associate with this bottom sheet. 6332 * - `locals` - `{string=}`: An object containing key/value pairs. The keys will 6333 * be used as names of values to inject into the controller. For example, 6334 * `locals: {three: 3}` would inject `three` into the controller with the value 6335 * of 3. 6336 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to 6337 * close it. Default true. 6338 * - `disableBackdrop` - `{boolean=}`: When set to true, the bottomsheet will not show a backdrop. 6339 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet. 6340 * Default true. 6341 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values 6342 * and the bottom sheet will not open until the promises resolve. 6343 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. 6344 * - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a `function`, `string`, 6345 * `object`, or null. Defaults to appending to the body of the root element (or the root element) of the application. 6346 * e.g. angular.element(document.getElementById('content')) or "#content" 6347 * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is open. 6348 * Default true. 6349 * 6350 * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or 6351 * rejected with `$mdBottomSheet.cancel()`. 6352 */ 6353 6354 /** 6355 * @ngdoc method 6356 * @name $mdBottomSheet#hide 6357 * 6358 * @description 6359 * Hide the existing bottom sheet and resolve the promise returned from 6360 * `$mdBottomSheet.show()`. This call will close the most recently opened/current bottomsheet (if any). 6361 * 6362 * @param {*=} response An argument for the resolved promise. 6363 * 6364 */ 6365 6366 /** 6367 * @ngdoc method 6368 * @name $mdBottomSheet#cancel 6369 * 6370 * @description 6371 * Hide the existing bottom sheet and reject the promise returned from 6372 * `$mdBottomSheet.show()`. 6373 * 6374 * @param {*=} response An argument for the rejected promise. 6375 * 6376 */ 6377 6378 function MdBottomSheetProvider($$interimElementProvider) { 6379 // how fast we need to flick down to close the sheet, pixels/ms 6380 var CLOSING_VELOCITY = 0.5; 6381 var PADDING = 80; // same as css 6382 6383 bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture"]; 6384 return $$interimElementProvider('$mdBottomSheet') 6385 .setDefaults({ 6386 methods: ['disableParentScroll', 'escapeToClose', 'clickOutsideToClose'], 6387 options: bottomSheetDefaults 6388 }); 6389 6390 /* @ngInject */ 6391 function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement, $mdGesture) { 6392 var backdrop; 6393 6394 return { 6395 themable: true, 6396 onShow: onShow, 6397 onRemove: onRemove, 6398 disableBackdrop: false, 6399 escapeToClose: true, 6400 clickOutsideToClose: true, 6401 disableParentScroll: true 6402 }; 6403 6404 6405 function onShow(scope, element, options, controller) { 6406 6407 element = $mdUtil.extractElementByName(element, 'md-bottom-sheet'); 6408 6409 // prevent tab focus or click focus on the bottom-sheet container 6410 element.attr('tabindex',"-1"); 6411 6412 if (!options.disableBackdrop) { 6413 // Add a backdrop that will close on click 6414 backdrop = $mdUtil.createBackdrop(scope, "_md-bottom-sheet-backdrop md-opaque"); 6415 6416 // Prevent mouse focus on backdrop; ONLY programatic focus allowed. 6417 // This allows clicks on backdrop to propagate to the $rootElement and 6418 // ESC key events to be detected properly. 6419 6420 backdrop[0].tabIndex = -1; 6421 6422 if (options.clickOutsideToClose) { 6423 backdrop.on('click', function() { 6424 $mdUtil.nextTick($mdBottomSheet.cancel,true); 6425 }); 6426 } 6427 6428 $mdTheming.inherit(backdrop, options.parent); 6429 6430 $animate.enter(backdrop, options.parent, null); 6431 } 6432 6433 var bottomSheet = new BottomSheet(element, options.parent); 6434 options.bottomSheet = bottomSheet; 6435 6436 $mdTheming.inherit(bottomSheet.element, options.parent); 6437 6438 if (options.disableParentScroll) { 6439 options.restoreScroll = $mdUtil.disableScrollAround(bottomSheet.element, options.parent); 6440 } 6441 6442 return $animate.enter(bottomSheet.element, options.parent, backdrop) 6443 .then(function() { 6444 var focusable = $mdUtil.findFocusTarget(element) || angular.element( 6445 element[0].querySelector('button') || 6446 element[0].querySelector('a') || 6447 element[0].querySelector('[ng-click]') 6448 ) || backdrop; 6449 6450 if (options.escapeToClose) { 6451 options.rootElementKeyupCallback = function(e) { 6452 if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) { 6453 $mdUtil.nextTick($mdBottomSheet.cancel,true); 6454 } 6455 }; 6456 6457 $rootElement.on('keyup', options.rootElementKeyupCallback); 6458 focusable && focusable.focus(); 6459 } 6460 }); 6461 6462 } 6463 6464 function onRemove(scope, element, options) { 6465 6466 var bottomSheet = options.bottomSheet; 6467 6468 if (!options.disableBackdrop) $animate.leave(backdrop); 6469 return $animate.leave(bottomSheet.element).then(function() { 6470 if (options.disableParentScroll) { 6471 options.restoreScroll(); 6472 delete options.restoreScroll; 6473 } 6474 6475 bottomSheet.cleanup(); 6476 }); 6477 } 6478 6479 /** 6480 * BottomSheet class to apply bottom-sheet behavior to an element 6481 */ 6482 function BottomSheet(element, parent) { 6483 var deregister = $mdGesture.register(parent, 'drag', { horizontal: false }); 6484 parent.on('$md.dragstart', onDragStart) 6485 .on('$md.drag', onDrag) 6486 .on('$md.dragend', onDragEnd); 6487 6488 return { 6489 element: element, 6490 cleanup: function cleanup() { 6491 deregister(); 6492 parent.off('$md.dragstart', onDragStart); 6493 parent.off('$md.drag', onDrag); 6494 parent.off('$md.dragend', onDragEnd); 6495 } 6496 }; 6497 6498 function onDragStart(ev) { 6499 // Disable transitions on transform so that it feels fast 6500 element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms'); 6501 } 6502 6503 function onDrag(ev) { 6504 var transform = ev.pointer.distanceY; 6505 if (transform < 5) { 6506 // Slow down drag when trying to drag up, and stop after PADDING 6507 transform = Math.max(-PADDING, transform / 2); 6508 } 6509 element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)'); 6510 } 6511 6512 function onDragEnd(ev) { 6513 if (ev.pointer.distanceY > 0 && 6514 (ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) { 6515 var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY; 6516 var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500); 6517 element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms'); 6518 $mdUtil.nextTick($mdBottomSheet.cancel,true); 6519 } else { 6520 element.css($mdConstant.CSS.TRANSITION_DURATION, ''); 6521 element.css($mdConstant.CSS.TRANSFORM, ''); 6522 } 6523 } 6524 } 6525 6526 } 6527 6528 } 6529 MdBottomSheetProvider.$inject = ["$$interimElementProvider"]; 6530 6531 })(); 6532 (function(){ 6533 "use strict"; 6534 6535 /** 6536 * @ngdoc module 6537 * @name material.components.card 6538 * 6539 * @description 6540 * Card components. 6541 */ 6542 angular.module('material.components.card', [ 6543 'material.core' 6544 ]) 6545 .directive('mdCard', mdCardDirective); 6546 6547 6548 /** 6549 * @ngdoc directive 6550 * @name mdCard 6551 * @module material.components.card 6552 * 6553 * @restrict E 6554 * 6555 * @description 6556 * The `<md-card>` directive is a container element used within `<md-content>` containers. 6557 * 6558 * An image included as a direct descendant will fill the card's width, while the `<md-card-content>` 6559 * container will wrap text content and provide padding. An `<md-card-footer>` element can be 6560 * optionally included to put content flush against the bottom edge of the card. 6561 * 6562 * Action buttons can be included in an `<md-card-actions>` element, similar to `<md-dialog-actions>`. 6563 * You can then position buttons using layout attributes. 6564 * 6565 * Card is built with: 6566 * * `<md-card-header>` - Header for the card, holds avatar, text and squared image 6567 * - `<md-card-avatar>` - Card avatar 6568 * - `md-user-avatar` - Class for user image 6569 * - `<md-icon>` 6570 * - `<md-card-header-text>` - Contains elements for the card description 6571 * - `md-title` - Class for the card title 6572 * - `md-subhead` - Class for the card sub header 6573 * * `<img>` - Image for the card 6574 * * `<md-card-title>` - Card content title 6575 * - `<md-card-title-text>` 6576 * - `md-headline` - Class for the card content title 6577 * - `md-subhead` - Class for the card content sub header 6578 * - `<md-card-title-media>` - Squared image within the title 6579 * - `md-media-sm` - Class for small image 6580 * - `md-media-md` - Class for medium image 6581 * - `md-media-lg` - Class for large image 6582 * * `<md-card-content>` - Card content 6583 * - `md-media-xl` - Class for extra large image 6584 * * `<md-card-actions>` - Card actions 6585 * - `<md-card-icon-actions>` - Icon actions 6586 * 6587 * Cards have constant width and variable heights; where the maximum height is limited to what can 6588 * fit within a single view on a platform, but it can temporarily expand as needed. 6589 * 6590 * @usage 6591 * ### Card with optional footer 6592 * <hljs lang="html"> 6593 * <md-card> 6594 * <img src="card-image.png" class="md-card-image" alt="image caption"> 6595 * <md-card-content> 6596 * <h2>Card headline</h2> 6597 * <p>Card content</p> 6598 * </md-card-content> 6599 * <md-card-footer> 6600 * Card footer 6601 * </md-card-footer> 6602 * </md-card> 6603 * </hljs> 6604 * 6605 * ### Card with actions 6606 * <hljs lang="html"> 6607 * <md-card> 6608 * <img src="card-image.png" class="md-card-image" alt="image caption"> 6609 * <md-card-content> 6610 * <h2>Card headline</h2> 6611 * <p>Card content</p> 6612 * </md-card-content> 6613 * <md-card-actions layout="row" layout-align="end center"> 6614 * <md-button>Action 1</md-button> 6615 * <md-button>Action 2</md-button> 6616 * </md-card-actions> 6617 * </md-card> 6618 * </hljs> 6619 * 6620 * ### Card with header, image, title actions and content 6621 * <hljs lang="html"> 6622 * <md-card> 6623 * <md-card-header> 6624 * <md-card-avatar> 6625 * <img class="md-user-avatar" src="avatar.png"/> 6626 * </md-card-avatar> 6627 * <md-card-header-text> 6628 * <span class="md-title">Title</span> 6629 * <span class="md-subhead">Sub header</span> 6630 * </md-card-header-text> 6631 * </md-card-header> 6632 * <img ng-src="card-image.png" class="md-card-image" alt="image caption"> 6633 * <md-card-title> 6634 * <md-card-title-text> 6635 * <span class="md-headline">Card headline</span> 6636 * <span class="md-subhead">Card subheader</span> 6637 * </md-card-title-text> 6638 * </md-card-title> 6639 * <md-card-actions layout="row" layout-align="start center"> 6640 * <md-button>Action 1</md-button> 6641 * <md-button>Action 2</md-button> 6642 * <md-card-icon-actions> 6643 * <md-button class="md-icon-button" aria-label="icon"> 6644 * <md-icon md-svg-icon="icon"></md-icon> 6645 * </md-button> 6646 * </md-card-icon-actions> 6647 * </md-card-actions> 6648 * <md-card-content> 6649 * <p> 6650 * Card content 6651 * </p> 6652 * </md-card-content> 6653 * </md-card> 6654 * </hljs> 6655 */ 6656 function mdCardDirective($mdTheming) { 6657 return { 6658 restrict: 'E', 6659 link: function ($scope, $element) { 6660 $mdTheming($element); 6661 } 6662 }; 6663 } 6664 mdCardDirective.$inject = ["$mdTheming"]; 6665 6666 })(); 6667 (function(){ 6668 "use strict"; 6669 6670 /** 6671 * @ngdoc module 6672 * @name material.components.checkbox 6673 * @description Checkbox module! 6674 */ 6675 angular 6676 .module('material.components.checkbox', ['material.core']) 6677 .directive('mdCheckbox', MdCheckboxDirective); 6678 6679 /** 6680 * @ngdoc directive 6681 * @name mdCheckbox 6682 * @module material.components.checkbox 6683 * @restrict E 6684 * 6685 * @description 6686 * The checkbox directive is used like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D). 6687 * 6688 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) 6689 * the checkbox is in the accent color by default. The primary color palette may be used with 6690 * the `md-primary` class. 6691 * 6692 * @param {string} ng-model Assignable angular expression to data-bind to. 6693 * @param {string=} name Property name of the form under which the control is published. 6694 * @param {expression=} ng-true-value The value to which the expression should be set when selected. 6695 * @param {expression=} ng-false-value The value to which the expression should be set when not selected. 6696 * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element. 6697 * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects 6698 * @param {string=} aria-label Adds label to checkbox for accessibility. 6699 * Defaults to checkbox's text. If no default text is found, a warning will be logged. 6700 * 6701 * @usage 6702 * <hljs lang="html"> 6703 * <md-checkbox ng-model="isChecked" aria-label="Finished?"> 6704 * Finished ? 6705 * </md-checkbox> 6706 * 6707 * <md-checkbox md-no-ink ng-model="hasInk" aria-label="No Ink Effects"> 6708 * No Ink Effects 6709 * </md-checkbox> 6710 * 6711 * <md-checkbox ng-disabled="true" ng-model="isDisabled" aria-label="Disabled"> 6712 * Disabled 6713 * </md-checkbox> 6714 * 6715 * </hljs> 6716 * 6717 */ 6718 function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $mdUtil, $timeout) { 6719 inputDirective = inputDirective[0]; 6720 var CHECKED_CSS = 'md-checked'; 6721 6722 return { 6723 restrict: 'E', 6724 transclude: true, 6725 require: '?ngModel', 6726 priority: 210, // Run before ngAria 6727 template: 6728 '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' + 6729 '<div class="md-icon"></div>' + 6730 '</div>' + 6731 '<div ng-transclude class="md-label"></div>', 6732 compile: compile 6733 }; 6734 6735 // ********************************************************** 6736 // Private Methods 6737 // ********************************************************** 6738 6739 function compile (tElement, tAttrs) { 6740 var container = tElement.children(); 6741 6742 tAttrs.type = 'checkbox'; 6743 tAttrs.tabindex = tAttrs.tabindex || '0'; 6744 tElement.attr('role', tAttrs.type); 6745 6746 // Attach a click handler in compile in order to immediately stop propagation 6747 // (especially for ng-click) when the checkbox is disabled. 6748 tElement.on('click', function(event) { 6749 if (this.hasAttribute('disabled')) { 6750 event.stopImmediatePropagation(); 6751 } 6752 }); 6753 6754 // Redirect focus events to the root element, because IE11 is always focusing the container element instead 6755 // of the md-checkbox element. This causes issues when using ngModelOptions: `updateOnBlur` 6756 container.on('focus', function() { 6757 tElement.focus(); 6758 }); 6759 6760 return function postLink(scope, element, attr, ngModelCtrl) { 6761 ngModelCtrl = ngModelCtrl || $mdUtil.fakeNgModel(); 6762 $mdTheming(element); 6763 6764 if (attr.ngChecked) { 6765 scope.$watch( 6766 scope.$eval.bind(scope, attr.ngChecked), 6767 ngModelCtrl.$setViewValue.bind(ngModelCtrl) 6768 ); 6769 } 6770 6771 $$watchExpr('ngDisabled', 'tabindex', { 6772 true: '-1', 6773 false: attr.tabindex 6774 }); 6775 6776 $mdAria.expectWithText(element, 'aria-label'); 6777 6778 // Reuse the original input[type=checkbox] directive from Angular core. 6779 // This is a bit hacky as we need our own event listener and own render 6780 // function. 6781 inputDirective.link.pre(scope, { 6782 on: angular.noop, 6783 0: {} 6784 }, attr, [ngModelCtrl]); 6785 6786 scope.mouseActive = false; 6787 element.on('click', listener) 6788 .on('keypress', keypressHandler) 6789 .on('mousedown', function() { 6790 scope.mouseActive = true; 6791 $timeout(function() { 6792 scope.mouseActive = false; 6793 }, 100); 6794 }) 6795 .on('focus', function() { 6796 if (scope.mouseActive === false) { 6797 element.addClass('md-focused'); 6798 } 6799 }) 6800 .on('blur', function() { 6801 element.removeClass('md-focused'); 6802 }); 6803 6804 ngModelCtrl.$render = render; 6805 6806 function $$watchExpr(expr, htmlAttr, valueOpts) { 6807 if (attr[expr]) { 6808 scope.$watch(attr[expr], function(val) { 6809 if (valueOpts[val]) { 6810 element.attr(htmlAttr, valueOpts[val]); 6811 } 6812 }); 6813 } 6814 } 6815 6816 function keypressHandler(ev) { 6817 var keyCode = ev.which || ev.keyCode; 6818 if (keyCode === $mdConstant.KEY_CODE.SPACE || keyCode === $mdConstant.KEY_CODE.ENTER) { 6819 ev.preventDefault(); 6820 6821 if (!element.hasClass('md-focused')) { 6822 element.addClass('md-focused'); 6823 } 6824 6825 listener(ev); 6826 } 6827 } 6828 function listener(ev) { 6829 if (element[0].hasAttribute('disabled')) { 6830 return; 6831 } 6832 6833 scope.$apply(function() { 6834 // Toggle the checkbox value... 6835 var viewValue = attr.ngChecked ? attr.checked : !ngModelCtrl.$viewValue; 6836 6837 ngModelCtrl.$setViewValue( viewValue, ev && ev.type); 6838 ngModelCtrl.$render(); 6839 }); 6840 } 6841 6842 function render() { 6843 if(ngModelCtrl.$viewValue) { 6844 element.addClass(CHECKED_CSS); 6845 } else { 6846 element.removeClass(CHECKED_CSS); 6847 } 6848 } 6849 }; 6850 } 6851 } 6852 MdCheckboxDirective.$inject = ["inputDirective", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$timeout"]; 6853 6854 })(); 6855 (function(){ 6856 "use strict"; 6857 6858 /** 6859 * @ngdoc module 6860 * @name material.components.content 6861 * 6862 * @description 6863 * Scrollable content 6864 */ 6865 angular.module('material.components.content', [ 6866 'material.core' 6867 ]) 6868 .directive('mdContent', mdContentDirective); 6869 6870 /** 6871 * @ngdoc directive 6872 * @name mdContent 6873 * @module material.components.content 6874 * 6875 * @restrict E 6876 * 6877 * @description 6878 * The `<md-content>` directive is a container element useful for scrollable content 6879 * 6880 * @usage 6881 * 6882 * - Add the `[layout-padding]` attribute to make the content padded. 6883 * 6884 * <hljs lang="html"> 6885 * <md-content layout-padding> 6886 * Lorem ipsum dolor sit amet, ne quod novum mei. 6887 * </md-content> 6888 * </hljs> 6889 * 6890 */ 6891 6892 function mdContentDirective($mdTheming) { 6893 return { 6894 restrict: 'E', 6895 controller: ['$scope', '$element', ContentController], 6896 link: function(scope, element, attr) { 6897 var node = element[0]; 6898 6899 $mdTheming(element); 6900 scope.$broadcast('$mdContentLoaded', element); 6901 6902 iosScrollFix(element[0]); 6903 } 6904 }; 6905 6906 function ContentController($scope, $element) { 6907 this.$scope = $scope; 6908 this.$element = $element; 6909 } 6910 } 6911 mdContentDirective.$inject = ["$mdTheming"]; 6912 6913 function iosScrollFix(node) { 6914 // IOS FIX: 6915 // If we scroll where there is no more room for the webview to scroll, 6916 // by default the webview itself will scroll up and down, this looks really 6917 // bad. So if we are scrolling to the very top or bottom, add/subtract one 6918 angular.element(node).on('$md.pressdown', function(ev) { 6919 // Only touch events 6920 if (ev.pointer.type !== 't') return; 6921 // Don't let a child content's touchstart ruin it for us. 6922 if (ev.$materialScrollFixed) return; 6923 ev.$materialScrollFixed = true; 6924 6925 if (node.scrollTop === 0) { 6926 node.scrollTop = 1; 6927 } else if (node.scrollHeight === node.scrollTop + node.offsetHeight) { 6928 node.scrollTop -= 1; 6929 } 6930 }); 6931 } 6932 6933 })(); 6934 (function(){ 6935 "use strict"; 6936 6937 /** 6938 * @ngdoc module 6939 * @name material.components.chips 6940 */ 6941 /* 6942 * @see js folder for chips implementation 6943 */ 6944 angular.module('material.components.chips', [ 6945 'material.core', 6946 'material.components.autocomplete' 6947 ]); 6948 6949 })(); 6950 (function(){ 6951 "use strict"; 6952 6953 (function() { 6954 'use strict'; 6955 6956 /** 6957 * @ngdoc module 6958 * @name material.components.datepicker 6959 * @description Datepicker 6960 */ 6961 angular.module('material.components.datepicker', [ 6962 'material.core', 6963 'material.components.icon', 6964 'material.components.virtualRepeat' 6965 ]).directive('mdCalendar', calendarDirective); 6966 6967 6968 // POST RELEASE 6969 // TODO(jelbourn): Mac Cmd + left / right == Home / End 6970 // TODO(jelbourn): Clicking on the month label opens the month-picker. 6971 // TODO(jelbourn): Minimum and maximum date 6972 // TODO(jelbourn): Refactor month element creation to use cloneNode (performance). 6973 // TODO(jelbourn): Define virtual scrolling constants (compactness) users can override. 6974 // TODO(jelbourn): Animated month transition on ng-model change (virtual-repeat) 6975 // TODO(jelbourn): Scroll snapping (virtual repeat) 6976 // TODO(jelbourn): Remove superfluous row from short months (virtual-repeat) 6977 // TODO(jelbourn): Month headers stick to top when scrolling. 6978 // TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view. 6979 // TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live 6980 // announcement and key handling). 6981 // Read-only calendar (not just date-picker). 6982 6983 /** 6984 * Height of one calendar month tbody. This must be made known to the virtual-repeat and is 6985 * subsequently used for scrolling to specific months. 6986 */ 6987 var TBODY_HEIGHT = 265; 6988 6989 /** 6990 * Height of a calendar month with a single row. This is needed to calculate the offset for 6991 * rendering an extra month in virtual-repeat that only contains one row. 6992 */ 6993 var TBODY_SINGLE_ROW_HEIGHT = 45; 6994 6995 function calendarDirective() { 6996 return { 6997 template: 6998 '<table aria-hidden="true" class="md-calendar-day-header"><thead></thead></table>' + 6999 '<div class="md-calendar-scroll-mask">' + 7000 '<md-virtual-repeat-container class="md-calendar-scroll-container" ' + 7001 'md-offset-size="' + (TBODY_SINGLE_ROW_HEIGHT - TBODY_HEIGHT) + '">' + 7002 '<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' + 7003 '<tbody role="rowgroup" md-virtual-repeat="i in ctrl.items" md-calendar-month ' + 7004 'md-month-offset="$index" class="md-calendar-month" ' + 7005 'md-start-index="ctrl.getSelectedMonthIndex()" ' + 7006 'md-item-size="' + TBODY_HEIGHT + '"></tbody>' + 7007 '</table>' + 7008 '</md-virtual-repeat-container>' + 7009 '</div>', 7010 scope: { 7011 minDate: '=mdMinDate', 7012 maxDate: '=mdMaxDate', 7013 dateFilter: '=mdDateFilter', 7014 }, 7015 require: ['ngModel', 'mdCalendar'], 7016 controller: CalendarCtrl, 7017 controllerAs: 'ctrl', 7018 bindToController: true, 7019 link: function(scope, element, attrs, controllers) { 7020 var ngModelCtrl = controllers[0]; 7021 var mdCalendarCtrl = controllers[1]; 7022 mdCalendarCtrl.configureNgModel(ngModelCtrl); 7023 } 7024 }; 7025 } 7026 7027 /** Class applied to the selected date cell/. */ 7028 var SELECTED_DATE_CLASS = 'md-calendar-selected-date'; 7029 7030 /** Class applied to the focused date cell/. */ 7031 var FOCUSED_DATE_CLASS = 'md-focus'; 7032 7033 /** Next identifier for calendar instance. */ 7034 var nextUniqueId = 0; 7035 7036 /** The first renderable date in the virtual-scrolling calendar (for all instances). */ 7037 var firstRenderableDate = null; 7038 7039 /** 7040 * Controller for the mdCalendar component. 7041 * @ngInject @constructor 7042 */ 7043 function CalendarCtrl($element, $attrs, $scope, $animate, $q, $mdConstant, 7044 $mdTheming, $$mdDateUtil, $mdDateLocale, $mdInkRipple, $mdUtil) { 7045 $mdTheming($element); 7046 /** 7047 * Dummy array-like object for virtual-repeat to iterate over. The length is the total 7048 * number of months that can be viewed. This is shorter than ideal because of (potential) 7049 * Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=1181658. 7050 */ 7051 this.items = {length: 2000}; 7052 7053 if (this.maxDate && this.minDate) { 7054 // Limit the number of months if min and max dates are set. 7055 var numMonths = $$mdDateUtil.getMonthDistance(this.minDate, this.maxDate) + 1; 7056 numMonths = Math.max(numMonths, 1); 7057 // Add an additional month as the final dummy month for rendering purposes. 7058 numMonths += 1; 7059 this.items.length = numMonths; 7060 } 7061 7062 /** @final {!angular.$animate} */ 7063 this.$animate = $animate; 7064 7065 /** @final {!angular.$q} */ 7066 this.$q = $q; 7067 7068 /** @final */ 7069 this.$mdInkRipple = $mdInkRipple; 7070 7071 /** @final */ 7072 this.$mdUtil = $mdUtil; 7073 7074 /** @final */ 7075 this.keyCode = $mdConstant.KEY_CODE; 7076 7077 /** @final */ 7078 this.dateUtil = $$mdDateUtil; 7079 7080 /** @final */ 7081 this.dateLocale = $mdDateLocale; 7082 7083 /** @final {!angular.JQLite} */ 7084 this.$element = $element; 7085 7086 /** @final {!angular.Scope} */ 7087 this.$scope = $scope; 7088 7089 /** @final {HTMLElement} */ 7090 this.calendarElement = $element[0].querySelector('.md-calendar'); 7091 7092 /** @final {HTMLElement} */ 7093 this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller'); 7094 7095 /** @final {Date} */ 7096 this.today = this.dateUtil.createDateAtMidnight(); 7097 7098 /** @type {Date} */ 7099 this.firstRenderableDate = this.dateUtil.incrementMonths(this.today, -this.items.length / 2); 7100 7101 if (this.minDate && this.minDate > this.firstRenderableDate) { 7102 this.firstRenderableDate = this.minDate; 7103 } else if (this.maxDate) { 7104 // Calculate the difference between the start date and max date. 7105 // Subtract 1 because it's an inclusive difference and 1 for the final dummy month. 7106 // 7107 var monthDifference = this.items.length - 2; 7108 this.firstRenderableDate = this.dateUtil.incrementMonths(this.maxDate, -(this.items.length - 2)); 7109 } 7110 7111 7112 /** @final {number} Unique ID for this calendar instance. */ 7113 this.id = nextUniqueId++; 7114 7115 /** @type {!angular.NgModelController} */ 7116 this.ngModelCtrl = null; 7117 7118 /** 7119 * The selected date. Keep track of this separately from the ng-model value so that we 7120 * can know, when the ng-model value changes, what the previous value was before it's updated 7121 * in the component's UI. 7122 * 7123 * @type {Date} 7124 */ 7125 this.selectedDate = null; 7126 7127 /** 7128 * The date that is currently focused or showing in the calendar. This will initially be set 7129 * to the ng-model value if set, otherwise to today. It will be updated as the user navigates 7130 * to other months. The cell corresponding to the displayDate does not necessarily always have 7131 * focus in the document (such as for cases when the user is scrolling the calendar). 7132 * @type {Date} 7133 */ 7134 this.displayDate = null; 7135 7136 /** 7137 * The date that has or should have focus. 7138 * @type {Date} 7139 */ 7140 this.focusDate = null; 7141 7142 /** @type {boolean} */ 7143 this.isInitialized = false; 7144 7145 /** @type {boolean} */ 7146 this.isMonthTransitionInProgress = false; 7147 7148 // Unless the user specifies so, the calendar should not be a tab stop. 7149 // This is necessary because ngAria might add a tabindex to anything with an ng-model 7150 // (based on whether or not the user has turned that particular feature on/off). 7151 if (!$attrs['tabindex']) { 7152 $element.attr('tabindex', '-1'); 7153 } 7154 7155 var self = this; 7156 7157 /** 7158 * Handles a click event on a date cell. 7159 * Created here so that every cell can use the same function instance. 7160 * @this {HTMLTableCellElement} The cell that was clicked. 7161 */ 7162 this.cellClickHandler = function() { 7163 var cellElement = this; 7164 if (this.hasAttribute('data-timestamp')) { 7165 $scope.$apply(function() { 7166 var timestamp = Number(cellElement.getAttribute('data-timestamp')); 7167 self.setNgModelValue(self.dateUtil.createDateAtMidnight(timestamp)); 7168 }); 7169 } 7170 }; 7171 7172 this.attachCalendarEventListeners(); 7173 } 7174 CalendarCtrl.$inject = ["$element", "$attrs", "$scope", "$animate", "$q", "$mdConstant", "$mdTheming", "$$mdDateUtil", "$mdDateLocale", "$mdInkRipple", "$mdUtil"]; 7175 7176 7177 /*** Initialization ***/ 7178 7179 /** 7180 * Sets up the controller's reference to ngModelController. 7181 * @param {!angular.NgModelController} ngModelCtrl 7182 */ 7183 CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl) { 7184 this.ngModelCtrl = ngModelCtrl; 7185 7186 var self = this; 7187 ngModelCtrl.$render = function() { 7188 self.changeSelectedDate(self.ngModelCtrl.$viewValue); 7189 }; 7190 }; 7191 7192 /** 7193 * Initialize the calendar by building the months that are initially visible. 7194 * Initialization should occur after the ngModel value is known. 7195 */ 7196 CalendarCtrl.prototype.buildInitialCalendarDisplay = function() { 7197 this.buildWeekHeader(); 7198 this.hideVerticalScrollbar(); 7199 7200 this.displayDate = this.selectedDate || this.today; 7201 this.isInitialized = true; 7202 }; 7203 7204 /** 7205 * Hides the vertical scrollbar on the calendar scroller by setting the width on the 7206 * calendar scroller and the `overflow: hidden` wrapper around the scroller, and then setting 7207 * a padding-right on the scroller equal to the width of the browser's scrollbar. 7208 * 7209 * This will cause a reflow. 7210 */ 7211 CalendarCtrl.prototype.hideVerticalScrollbar = function() { 7212 var element = this.$element[0]; 7213 7214 var scrollMask = element.querySelector('.md-calendar-scroll-mask'); 7215 var scroller = this.calendarScroller; 7216 7217 var headerWidth = element.querySelector('.md-calendar-day-header').clientWidth; 7218 var scrollbarWidth = scroller.offsetWidth - scroller.clientWidth; 7219 7220 scrollMask.style.width = headerWidth + 'px'; 7221 scroller.style.width = (headerWidth + scrollbarWidth) + 'px'; 7222 scroller.style.paddingRight = scrollbarWidth + 'px'; 7223 }; 7224 7225 7226 /** Attach event listeners for the calendar. */ 7227 CalendarCtrl.prototype.attachCalendarEventListeners = function() { 7228 // Keyboard interaction. 7229 this.$element.on('keydown', angular.bind(this, this.handleKeyEvent)); 7230 }; 7231 7232 /*** User input handling ***/ 7233 7234 /** 7235 * Handles a key event in the calendar with the appropriate action. The action will either 7236 * be to select the focused date or to navigate to focus a new date. 7237 * @param {KeyboardEvent} event 7238 */ 7239 CalendarCtrl.prototype.handleKeyEvent = function(event) { 7240 var self = this; 7241 this.$scope.$apply(function() { 7242 // Capture escape and emit back up so that a wrapping component 7243 // (such as a date-picker) can decide to close. 7244 if (event.which == self.keyCode.ESCAPE || event.which == self.keyCode.TAB) { 7245 self.$scope.$emit('md-calendar-close'); 7246 7247 if (event.which == self.keyCode.TAB) { 7248 event.preventDefault(); 7249 } 7250 7251 return; 7252 } 7253 7254 // Remaining key events fall into two categories: selection and navigation. 7255 // Start by checking if this is a selection event. 7256 if (event.which === self.keyCode.ENTER) { 7257 self.setNgModelValue(self.displayDate); 7258 event.preventDefault(); 7259 return; 7260 } 7261 7262 // Selection isn't occurring, so the key event is either navigation or nothing. 7263 var date = self.getFocusDateFromKeyEvent(event); 7264 if (date) { 7265 date = self.boundDateByMinAndMax(date); 7266 event.preventDefault(); 7267 event.stopPropagation(); 7268 7269 // Since this is a keyboard interaction, actually give the newly focused date keyboard 7270 // focus after the been brought into view. 7271 self.changeDisplayDate(date).then(function () { 7272 self.focus(date); 7273 }); 7274 } 7275 }); 7276 }; 7277 7278 /** 7279 * Gets the date to focus as the result of a key event. 7280 * @param {KeyboardEvent} event 7281 * @returns {Date} Date to navigate to, or null if the key does not match a calendar shortcut. 7282 */ 7283 CalendarCtrl.prototype.getFocusDateFromKeyEvent = function(event) { 7284 var dateUtil = this.dateUtil; 7285 var keyCode = this.keyCode; 7286 7287 switch (event.which) { 7288 case keyCode.RIGHT_ARROW: return dateUtil.incrementDays(this.displayDate, 1); 7289 case keyCode.LEFT_ARROW: return dateUtil.incrementDays(this.displayDate, -1); 7290 case keyCode.DOWN_ARROW: 7291 return event.metaKey ? 7292 dateUtil.incrementMonths(this.displayDate, 1) : 7293 dateUtil.incrementDays(this.displayDate, 7); 7294 case keyCode.UP_ARROW: 7295 return event.metaKey ? 7296 dateUtil.incrementMonths(this.displayDate, -1) : 7297 dateUtil.incrementDays(this.displayDate, -7); 7298 case keyCode.PAGE_DOWN: return dateUtil.incrementMonths(this.displayDate, 1); 7299 case keyCode.PAGE_UP: return dateUtil.incrementMonths(this.displayDate, -1); 7300 case keyCode.HOME: return dateUtil.getFirstDateOfMonth(this.displayDate); 7301 case keyCode.END: return dateUtil.getLastDateOfMonth(this.displayDate); 7302 default: return null; 7303 } 7304 }; 7305 7306 /** 7307 * Gets the "index" of the currently selected date as it would be in the virtual-repeat. 7308 * @returns {number} 7309 */ 7310 CalendarCtrl.prototype.getSelectedMonthIndex = function() { 7311 return this.dateUtil.getMonthDistance(this.firstRenderableDate, 7312 this.selectedDate || this.today); 7313 }; 7314 7315 /** 7316 * Scrolls to the month of the given date. 7317 * @param {Date} date 7318 */ 7319 CalendarCtrl.prototype.scrollToMonth = function(date) { 7320 if (!this.dateUtil.isValidDate(date)) { 7321 return; 7322 } 7323 7324 var monthDistance = this.dateUtil.getMonthDistance(this.firstRenderableDate, date); 7325 this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT; 7326 }; 7327 7328 /** 7329 * Sets the ng-model value for the calendar and emits a change event. 7330 * @param {Date} date 7331 */ 7332 CalendarCtrl.prototype.setNgModelValue = function(date) { 7333 this.$scope.$emit('md-calendar-change', date); 7334 this.ngModelCtrl.$setViewValue(date); 7335 this.ngModelCtrl.$render(); 7336 }; 7337 7338 /** 7339 * Focus the cell corresponding to the given date. 7340 * @param {Date=} opt_date 7341 */ 7342 CalendarCtrl.prototype.focus = function(opt_date) { 7343 var date = opt_date || this.selectedDate || this.today; 7344 7345 var previousFocus = this.calendarElement.querySelector('.md-focus'); 7346 if (previousFocus) { 7347 previousFocus.classList.remove(FOCUSED_DATE_CLASS); 7348 } 7349 7350 var cellId = this.getDateId(date); 7351 var cell = document.getElementById(cellId); 7352 if (cell) { 7353 cell.classList.add(FOCUSED_DATE_CLASS); 7354 cell.focus(); 7355 } else { 7356 this.focusDate = date; 7357 } 7358 }; 7359 7360 /** 7361 * If a date exceeds minDate or maxDate, returns date matching minDate or maxDate, respectively. 7362 * Otherwise, returns the date. 7363 * @param {Date} date 7364 * @return {Date} 7365 */ 7366 CalendarCtrl.prototype.boundDateByMinAndMax = function(date) { 7367 var boundDate = date; 7368 if (this.minDate && date < this.minDate) { 7369 boundDate = new Date(this.minDate.getTime()); 7370 } 7371 if (this.maxDate && date > this.maxDate) { 7372 boundDate = new Date(this.maxDate.getTime()); 7373 } 7374 return boundDate; 7375 }; 7376 7377 /*** Updating the displayed / selected date ***/ 7378 7379 /** 7380 * Change the selected date in the calendar (ngModel value has already been changed). 7381 * @param {Date} date 7382 */ 7383 CalendarCtrl.prototype.changeSelectedDate = function(date) { 7384 var self = this; 7385 var previousSelectedDate = this.selectedDate; 7386 this.selectedDate = date; 7387 this.changeDisplayDate(date).then(function() { 7388 7389 // Remove the selected class from the previously selected date, if any. 7390 if (previousSelectedDate) { 7391 var prevDateCell = 7392 document.getElementById(self.getDateId(previousSelectedDate)); 7393 if (prevDateCell) { 7394 prevDateCell.classList.remove(SELECTED_DATE_CLASS); 7395 prevDateCell.setAttribute('aria-selected', 'false'); 7396 } 7397 } 7398 7399 // Apply the select class to the new selected date if it is set. 7400 if (date) { 7401 var dateCell = document.getElementById(self.getDateId(date)); 7402 if (dateCell) { 7403 dateCell.classList.add(SELECTED_DATE_CLASS); 7404 dateCell.setAttribute('aria-selected', 'true'); 7405 } 7406 } 7407 }); 7408 }; 7409 7410 7411 /** 7412 * Change the date that is being shown in the calendar. If the given date is in a different 7413 * month, the displayed month will be transitioned. 7414 * @param {Date} date 7415 */ 7416 CalendarCtrl.prototype.changeDisplayDate = function(date) { 7417 // Initialization is deferred until this function is called because we want to reflect 7418 // the starting value of ngModel. 7419 if (!this.isInitialized) { 7420 this.buildInitialCalendarDisplay(); 7421 return this.$q.when(); 7422 } 7423 7424 // If trying to show an invalid date or a transition is in progress, do nothing. 7425 if (!this.dateUtil.isValidDate(date) || this.isMonthTransitionInProgress) { 7426 return this.$q.when(); 7427 } 7428 7429 this.isMonthTransitionInProgress = true; 7430 var animationPromise = this.animateDateChange(date); 7431 7432 this.displayDate = date; 7433 7434 var self = this; 7435 animationPromise.then(function() { 7436 self.isMonthTransitionInProgress = false; 7437 }); 7438 7439 return animationPromise; 7440 }; 7441 7442 /** 7443 * Animates the transition from the calendar's current month to the given month. 7444 * @param {Date} date 7445 * @returns {angular.$q.Promise} The animation promise. 7446 */ 7447 CalendarCtrl.prototype.animateDateChange = function(date) { 7448 this.scrollToMonth(date); 7449 return this.$q.when(); 7450 }; 7451 7452 /*** Constructing the calendar table ***/ 7453 7454 /** 7455 * Builds and appends a day-of-the-week header to the calendar. 7456 * This should only need to be called once during initialization. 7457 */ 7458 CalendarCtrl.prototype.buildWeekHeader = function() { 7459 var firstDayOfWeek = this.dateLocale.firstDayOfWeek; 7460 var shortDays = this.dateLocale.shortDays; 7461 7462 var row = document.createElement('tr'); 7463 for (var i = 0; i < 7; i++) { 7464 var th = document.createElement('th'); 7465 th.textContent = shortDays[(i + firstDayOfWeek) % 7]; 7466 row.appendChild(th); 7467 } 7468 7469 this.$element.find('thead').append(row); 7470 }; 7471 7472 /** 7473 * Gets an identifier for a date unique to the calendar instance for internal 7474 * purposes. Not to be displayed. 7475 * @param {Date} date 7476 * @returns {string} 7477 */ 7478 CalendarCtrl.prototype.getDateId = function(date) { 7479 return [ 7480 'md', 7481 this.id, 7482 date.getFullYear(), 7483 date.getMonth(), 7484 date.getDate() 7485 ].join('-'); 7486 }; 7487 })(); 7488 7489 })(); 7490 (function(){ 7491 "use strict"; 7492 7493 (function() { 7494 'use strict'; 7495 7496 7497 angular.module('material.components.datepicker') 7498 .directive('mdCalendarMonth', mdCalendarMonthDirective); 7499 7500 7501 /** 7502 * Private directive consumed by md-calendar. Having this directive lets the calender use 7503 * md-virtual-repeat and also cleanly separates the month DOM construction functions from 7504 * the rest of the calendar controller logic. 7505 */ 7506 function mdCalendarMonthDirective() { 7507 return { 7508 require: ['^^mdCalendar', 'mdCalendarMonth'], 7509 scope: {offset: '=mdMonthOffset'}, 7510 controller: CalendarMonthCtrl, 7511 controllerAs: 'mdMonthCtrl', 7512 bindToController: true, 7513 link: function(scope, element, attrs, controllers) { 7514 var calendarCtrl = controllers[0]; 7515 var monthCtrl = controllers[1]; 7516 7517 monthCtrl.calendarCtrl = calendarCtrl; 7518 monthCtrl.generateContent(); 7519 7520 // The virtual-repeat re-uses the same DOM elements, so there are only a limited number 7521 // of repeated items that are linked, and then those elements have their bindings updataed. 7522 // Since the months are not generated by bindings, we simply regenerate the entire thing 7523 // when the binding (offset) changes. 7524 scope.$watch(function() { return monthCtrl.offset; }, function(offset, oldOffset) { 7525 if (offset != oldOffset) { 7526 monthCtrl.generateContent(); 7527 } 7528 }); 7529 } 7530 }; 7531 } 7532 7533 /** Class applied to the cell for today. */ 7534 var TODAY_CLASS = 'md-calendar-date-today'; 7535 7536 /** Class applied to the selected date cell/. */ 7537 var SELECTED_DATE_CLASS = 'md-calendar-selected-date'; 7538 7539 /** Class applied to the focused date cell/. */ 7540 var FOCUSED_DATE_CLASS = 'md-focus'; 7541 7542 /** 7543 * Controller for a single calendar month. 7544 * @ngInject @constructor 7545 */ 7546 function CalendarMonthCtrl($element, $$mdDateUtil, $mdDateLocale) { 7547 this.dateUtil = $$mdDateUtil; 7548 this.dateLocale = $mdDateLocale; 7549 this.$element = $element; 7550 this.calendarCtrl = null; 7551 7552 /** 7553 * Number of months from the start of the month "items" that the currently rendered month 7554 * occurs. Set via angular data binding. 7555 * @type {number} 7556 */ 7557 this.offset; 7558 7559 /** 7560 * Date cell to focus after appending the month to the document. 7561 * @type {HTMLElement} 7562 */ 7563 this.focusAfterAppend = null; 7564 } 7565 CalendarMonthCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"]; 7566 7567 /** Generate and append the content for this month to the directive element. */ 7568 CalendarMonthCtrl.prototype.generateContent = function() { 7569 var calendarCtrl = this.calendarCtrl; 7570 var date = this.dateUtil.incrementMonths(calendarCtrl.firstRenderableDate, this.offset); 7571 7572 this.$element.empty(); 7573 this.$element.append(this.buildCalendarForMonth(date)); 7574 7575 if (this.focusAfterAppend) { 7576 this.focusAfterAppend.classList.add(FOCUSED_DATE_CLASS); 7577 this.focusAfterAppend.focus(); 7578 this.focusAfterAppend = null; 7579 } 7580 }; 7581 7582 /** 7583 * Creates a single cell to contain a date in the calendar with all appropriate 7584 * attributes and classes added. If a date is given, the cell content will be set 7585 * based on the date. 7586 * @param {Date=} opt_date 7587 * @returns {HTMLElement} 7588 */ 7589 CalendarMonthCtrl.prototype.buildDateCell = function(opt_date) { 7590 var calendarCtrl = this.calendarCtrl; 7591 7592 // TODO(jelbourn): cloneNode is likely a faster way of doing this. 7593 var cell = document.createElement('td'); 7594 cell.tabIndex = -1; 7595 cell.classList.add('md-calendar-date'); 7596 cell.setAttribute('role', 'gridcell'); 7597 7598 if (opt_date) { 7599 cell.setAttribute('tabindex', '-1'); 7600 cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date)); 7601 cell.id = calendarCtrl.getDateId(opt_date); 7602 7603 // Use `data-timestamp` attribute because IE10 does not support the `dataset` property. 7604 cell.setAttribute('data-timestamp', opt_date.getTime()); 7605 7606 // TODO(jelourn): Doing these comparisons for class addition during generation might be slow. 7607 // It may be better to finish the construction and then query the node and add the class. 7608 if (this.dateUtil.isSameDay(opt_date, calendarCtrl.today)) { 7609 cell.classList.add(TODAY_CLASS); 7610 } 7611 7612 if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) && 7613 this.dateUtil.isSameDay(opt_date, calendarCtrl.selectedDate)) { 7614 cell.classList.add(SELECTED_DATE_CLASS); 7615 cell.setAttribute('aria-selected', 'true'); 7616 } 7617 7618 var cellText = this.dateLocale.dates[opt_date.getDate()]; 7619 7620 if (this.isDateEnabled(opt_date)) { 7621 // Add a indicator for select, hover, and focus states. 7622 var selectionIndicator = document.createElement('span'); 7623 cell.appendChild(selectionIndicator); 7624 selectionIndicator.classList.add('md-calendar-date-selection-indicator'); 7625 selectionIndicator.textContent = cellText; 7626 7627 cell.addEventListener('click', calendarCtrl.cellClickHandler); 7628 7629 if (calendarCtrl.focusDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.focusDate)) { 7630 this.focusAfterAppend = cell; 7631 } 7632 } else { 7633 cell.classList.add('md-calendar-date-disabled'); 7634 cell.textContent = cellText; 7635 } 7636 } 7637 7638 return cell; 7639 }; 7640 7641 /** 7642 * Check whether date is in range and enabled 7643 * @param {Date=} opt_date 7644 * @return {boolean} Whether the date is enabled. 7645 */ 7646 CalendarMonthCtrl.prototype.isDateEnabled = function(opt_date) { 7647 return this.dateUtil.isDateWithinRange(opt_date, 7648 this.calendarCtrl.minDate, this.calendarCtrl.maxDate) && 7649 (!angular.isFunction(this.calendarCtrl.dateFilter) 7650 || this.calendarCtrl.dateFilter(opt_date)); 7651 } 7652 7653 /** 7654 * Builds a `tr` element for the calendar grid. 7655 * @param rowNumber The week number within the month. 7656 * @returns {HTMLElement} 7657 */ 7658 CalendarMonthCtrl.prototype.buildDateRow = function(rowNumber) { 7659 var row = document.createElement('tr'); 7660 row.setAttribute('role', 'row'); 7661 7662 // Because of an NVDA bug (with Firefox), the row needs an aria-label in order 7663 // to prevent the entire row being read aloud when the user moves between rows. 7664 // See http://community.nvda-project.org/ticket/4643. 7665 row.setAttribute('aria-label', this.dateLocale.weekNumberFormatter(rowNumber)); 7666 7667 return row; 7668 }; 7669 7670 /** 7671 * Builds the <tbody> content for the given date's month. 7672 * @param {Date=} opt_dateInMonth 7673 * @returns {DocumentFragment} A document fragment containing the <tr> elements. 7674 */ 7675 CalendarMonthCtrl.prototype.buildCalendarForMonth = function(opt_dateInMonth) { 7676 var date = this.dateUtil.isValidDate(opt_dateInMonth) ? opt_dateInMonth : new Date(); 7677 7678 var firstDayOfMonth = this.dateUtil.getFirstDateOfMonth(date); 7679 var firstDayOfTheWeek = this.getLocaleDay_(firstDayOfMonth); 7680 var numberOfDaysInMonth = this.dateUtil.getNumberOfDaysInMonth(date); 7681 7682 // Store rows for the month in a document fragment so that we can append them all at once. 7683 var monthBody = document.createDocumentFragment(); 7684 7685 var rowNumber = 1; 7686 var row = this.buildDateRow(rowNumber); 7687 monthBody.appendChild(row); 7688 7689 // If this is the final month in the list of items, only the first week should render, 7690 // so we should return immediately after the first row is complete and has been 7691 // attached to the body. 7692 var isFinalMonth = this.offset === this.calendarCtrl.items.length - 1; 7693 7694 // Add a label for the month. If the month starts on a Sun/Mon/Tues, the month label 7695 // goes on a row above the first of the month. Otherwise, the month label takes up the first 7696 // two cells of the first row. 7697 var blankCellOffset = 0; 7698 var monthLabelCell = document.createElement('td'); 7699 monthLabelCell.classList.add('md-calendar-month-label'); 7700 // If the entire month is after the max date, render the label as a disabled state. 7701 if (this.calendarCtrl.maxDate && firstDayOfMonth > this.calendarCtrl.maxDate) { 7702 monthLabelCell.classList.add('md-calendar-month-label-disabled'); 7703 } 7704 monthLabelCell.textContent = this.dateLocale.monthHeaderFormatter(date); 7705 if (firstDayOfTheWeek <= 2) { 7706 monthLabelCell.setAttribute('colspan', '7'); 7707 7708 var monthLabelRow = this.buildDateRow(); 7709 monthLabelRow.appendChild(monthLabelCell); 7710 monthBody.insertBefore(monthLabelRow, row); 7711 7712 if (isFinalMonth) { 7713 return monthBody; 7714 } 7715 } else { 7716 blankCellOffset = 2; 7717 monthLabelCell.setAttribute('colspan', '2'); 7718 row.appendChild(monthLabelCell); 7719 } 7720 7721 // Add a blank cell for each day of the week that occurs before the first of the month. 7722 // For example, if the first day of the month is a Tuesday, add blank cells for Sun and Mon. 7723 // The blankCellOffset is needed in cases where the first N cells are used by the month label. 7724 for (var i = blankCellOffset; i < firstDayOfTheWeek; i++) { 7725 row.appendChild(this.buildDateCell()); 7726 } 7727 7728 // Add a cell for each day of the month, keeping track of the day of the week so that 7729 // we know when to start a new row. 7730 var dayOfWeek = firstDayOfTheWeek; 7731 var iterationDate = firstDayOfMonth; 7732 for (var d = 1; d <= numberOfDaysInMonth; d++) { 7733 // If we've reached the end of the week, start a new row. 7734 if (dayOfWeek === 7) { 7735 // We've finished the first row, so we're done if this is the final month. 7736 if (isFinalMonth) { 7737 return monthBody; 7738 } 7739 dayOfWeek = 0; 7740 rowNumber++; 7741 row = this.buildDateRow(rowNumber); 7742 monthBody.appendChild(row); 7743 } 7744 7745 iterationDate.setDate(d); 7746 var cell = this.buildDateCell(iterationDate); 7747 row.appendChild(cell); 7748 7749 dayOfWeek++; 7750 } 7751 7752 // Ensure that the last row of the month has 7 cells. 7753 while (row.childNodes.length < 7) { 7754 row.appendChild(this.buildDateCell()); 7755 } 7756 7757 // Ensure that all months have 6 rows. This is necessary for now because the virtual-repeat 7758 // requires that all items have exactly the same height. 7759 while (monthBody.childNodes.length < 6) { 7760 var whitespaceRow = this.buildDateRow(); 7761 for (var i = 0; i < 7; i++) { 7762 whitespaceRow.appendChild(this.buildDateCell()); 7763 } 7764 monthBody.appendChild(whitespaceRow); 7765 } 7766 7767 return monthBody; 7768 }; 7769 7770 /** 7771 * Gets the day-of-the-week index for a date for the current locale. 7772 * @private 7773 * @param {Date} date 7774 * @returns {number} The column index of the date in the calendar. 7775 */ 7776 CalendarMonthCtrl.prototype.getLocaleDay_ = function(date) { 7777 return (date.getDay() + (7 - this.dateLocale.firstDayOfWeek)) % 7 7778 }; 7779 })(); 7780 7781 })(); 7782 (function(){ 7783 "use strict"; 7784 7785 (function() { 7786 'use strict'; 7787 7788 /** 7789 * @ngdoc service 7790 * @name $mdDateLocaleProvider 7791 * @module material.components.datepicker 7792 * 7793 * @description 7794 * The `$mdDateLocaleProvider` is the provider that creates the `$mdDateLocale` service. 7795 * This provider that allows the user to specify messages, formatters, and parsers for date 7796 * internationalization. The `$mdDateLocale` service itself is consumed by Angular Material 7797 * components that deal with dates. 7798 * 7799 * @property {(Array<string>)=} months Array of month names (in order). 7800 * @property {(Array<string>)=} shortMonths Array of abbreviated month names. 7801 * @property {(Array<string>)=} days Array of the days of the week (in order). 7802 * @property {(Array<string>)=} shortDays Array of abbreviated dayes of the week. 7803 * @property {(Array<string>)=} dates Array of dates of the month. Only necessary for locales 7804 * using a numeral system other than [1, 2, 3...]. 7805 * @property {(Array<string>)=} firstDayOfWeek The first day of the week. Sunday = 0, Monday = 1, 7806 * etc. 7807 * @property {(function(string): Date)=} parseDate Function to parse a date object from a string. 7808 * @property {(function(Date): string)=} formatDate Function to format a date object to a string. 7809 * @property {(function(Date): string)=} monthHeaderFormatter Function that returns the label for 7810 * a month given a date. 7811 * @property {(function(number): string)=} weekNumberFormatter Function that returns a label for 7812 * a week given the week number. 7813 * @property {(string)=} msgCalendar Translation of the label "Calendar" for the current locale. 7814 * @property {(string)=} msgOpenCalendar Translation of the button label "Open calendar" for the 7815 * current locale. 7816 * 7817 * @usage 7818 * <hljs lang="js"> 7819 * myAppModule.config(function($mdDateLocaleProvider) { 7820 * 7821 * // Example of a French localization. 7822 * $mdDateLocaleProvider.months = ['janvier', 'février', 'mars', ...]; 7823 * $mdDateLocaleProvider.shortMonths = ['janv', 'févr', 'mars', ...]; 7824 * $mdDateLocaleProvider.days = ['dimanche', 'lundi', 'mardi', ...]; 7825 * $mdDateLocaleProvider.shortDays = ['Di', 'Lu', 'Ma', ...]; 7826 * 7827 * // Can change week display to start on Monday. 7828 * $mdDateLocaleProvider.firstDayOfWeek = 1; 7829 * 7830 * // Optional. 7831 * $mdDateLocaleProvider.dates = [1, 2, 3, 4, 5, 6, ...]; 7832 * 7833 * // Example uses moment.js to parse and format dates. 7834 * $mdDateLocaleProvider.parseDate = function(dateString) { 7835 * var m = moment(dateString, 'L', true); 7836 * return m.isValid() ? m.toDate() : new Date(NaN); 7837 * }; 7838 * 7839 * $mdDateLocaleProvider.formatDate = function(date) { 7840 * return moment(date).format('L'); 7841 * }; 7842 * 7843 * $mdDateLocaleProvider.monthHeaderFormatter = function(date) { 7844 * return myShortMonths[date.getMonth()] + ' ' + date.getFullYear(); 7845 * }; 7846 * 7847 * // In addition to date display, date components also need localized messages 7848 * // for aria-labels for screen-reader users. 7849 * 7850 * $mdDateLocaleProvider.weekNumberFormatter = function(weekNumber) { 7851 * return 'Semaine ' + weekNumber; 7852 * }; 7853 * 7854 * $mdDateLocaleProvider.msgCalendar = 'Calendrier'; 7855 * $mdDateLocaleProvider.msgOpenCalendar = 'Ouvrir le calendrier'; 7856 * 7857 * }); 7858 * </hljs> 7859 * 7860 */ 7861 7862 angular.module('material.components.datepicker').config(["$provide", function($provide) { 7863 // TODO(jelbourn): Assert provided values are correctly formatted. Need assertions. 7864 7865 /** @constructor */ 7866 function DateLocaleProvider() { 7867 /** Array of full month names. E.g., ['January', 'February', ...] */ 7868 this.months = null; 7869 7870 /** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */ 7871 this.shortMonths = null; 7872 7873 /** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */ 7874 this.days = null; 7875 7876 /** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */ 7877 this.shortDays = null; 7878 7879 /** Array of dates of a month (1 - 31). Characters might be different in some locales. */ 7880 this.dates = null; 7881 7882 /** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */ 7883 this.firstDayOfWeek = 0; 7884 7885 /** 7886 * Function that converts the date portion of a Date to a string. 7887 * @type {(function(Date): string)} 7888 */ 7889 this.formatDate = null; 7890 7891 /** 7892 * Function that converts a date string to a Date object (the date portion) 7893 * @type {function(string): Date} 7894 */ 7895 this.parseDate = null; 7896 7897 /** 7898 * Function that formats a Date into a month header string. 7899 * @type {function(Date): string} 7900 */ 7901 this.monthHeaderFormatter = null; 7902 7903 /** 7904 * Function that formats a week number into a label for the week. 7905 * @type {function(number): string} 7906 */ 7907 this.weekNumberFormatter = null; 7908 7909 /** 7910 * Function that formats a date into a long aria-label that is read 7911 * when the focused date changes. 7912 * @type {function(Date): string} 7913 */ 7914 this.longDateFormatter = null; 7915 7916 /** 7917 * ARIA label for the calendar "dialog" used in the datepicker. 7918 * @type {string} 7919 */ 7920 this.msgCalendar = ''; 7921 7922 /** 7923 * ARIA label for the datepicker's "Open calendar" buttons. 7924 * @type {string} 7925 */ 7926 this.msgOpenCalendar = ''; 7927 } 7928 7929 /** 7930 * Factory function that returns an instance of the dateLocale service. 7931 * @ngInject 7932 * @param $locale 7933 * @returns {DateLocale} 7934 */ 7935 DateLocaleProvider.prototype.$get = function($locale) { 7936 /** 7937 * Default date-to-string formatting function. 7938 * @param {!Date} date 7939 * @returns {string} 7940 */ 7941 function defaultFormatDate(date) { 7942 if (!date) { 7943 return ''; 7944 } 7945 7946 // All of the dates created through ng-material *should* be set to midnight. 7947 // If we encounter a date where the localeTime shows at 11pm instead of midnight, 7948 // we have run into an issue with DST where we need to increment the hour by one: 7949 // var d = new Date(1992, 9, 8, 0, 0, 0); 7950 // d.toLocaleString(); // == "10/7/1992, 11:00:00 PM" 7951 var localeTime = date.toLocaleTimeString(); 7952 var formatDate = date; 7953 if (date.getHours() == 0 && 7954 (localeTime.indexOf('11:') !== -1 || localeTime.indexOf('23:') !== -1)) { 7955 formatDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0); 7956 } 7957 7958 return formatDate.toLocaleDateString(); 7959 } 7960 7961 /** 7962 * Default string-to-date parsing function. 7963 * @param {string} dateString 7964 * @returns {!Date} 7965 */ 7966 function defaultParseDate(dateString) { 7967 return new Date(dateString); 7968 } 7969 7970 /** 7971 * Default function to determine whether a string makes sense to be 7972 * parsed to a Date object. 7973 * 7974 * This is very permissive and is just a basic sanity check to ensure that 7975 * things like single integers aren't able to be parsed into dates. 7976 * @param {string} dateString 7977 * @returns {boolean} 7978 */ 7979 function defaultIsDateComplete(dateString) { 7980 dateString = dateString.trim(); 7981 7982 // Looks for three chunks of content (either numbers or text) separated 7983 // by delimiters. 7984 var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ \.,]+|[\/\-])){2}([a-zA-Z]{3,}|[0-9]{1,4})$/; 7985 return re.test(dateString); 7986 } 7987 7988 /** 7989 * Default date-to-string formatter to get a month header. 7990 * @param {!Date} date 7991 * @returns {string} 7992 */ 7993 function defaultMonthHeaderFormatter(date) { 7994 return service.shortMonths[date.getMonth()] + ' ' + date.getFullYear(); 7995 } 7996 7997 /** 7998 * Default week number formatter. 7999 * @param number 8000 * @returns {string} 8001 */ 8002 function defaultWeekNumberFormatter(number) { 8003 return 'Week ' + number; 8004 } 8005 8006 /** 8007 * Default formatter for date cell aria-labels. 8008 * @param {!Date} date 8009 * @returns {string} 8010 */ 8011 function defaultLongDateFormatter(date) { 8012 // Example: 'Thursday June 18 2015' 8013 return [ 8014 service.days[date.getDay()], 8015 service.months[date.getMonth()], 8016 service.dates[date.getDate()], 8017 date.getFullYear() 8018 ].join(' '); 8019 } 8020 8021 // The default "short" day strings are the first character of each day, 8022 // e.g., "Monday" => "M". 8023 var defaultShortDays = $locale.DATETIME_FORMATS.DAY.map(function(day) { 8024 return day[0]; 8025 }); 8026 8027 // The default dates are simply the numbers 1 through 31. 8028 var defaultDates = Array(32); 8029 for (var i = 1; i <= 31; i++) { 8030 defaultDates[i] = i; 8031 } 8032 8033 // Default ARIA messages are in English (US). 8034 var defaultMsgCalendar = 'Calendar'; 8035 var defaultMsgOpenCalendar = 'Open calendar'; 8036 8037 var service = { 8038 months: this.months || $locale.DATETIME_FORMATS.MONTH, 8039 shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH, 8040 days: this.days || $locale.DATETIME_FORMATS.DAY, 8041 shortDays: this.shortDays || defaultShortDays, 8042 dates: this.dates || defaultDates, 8043 firstDayOfWeek: this.firstDayOfWeek || 0, 8044 formatDate: this.formatDate || defaultFormatDate, 8045 parseDate: this.parseDate || defaultParseDate, 8046 isDateComplete: this.isDateComplete || defaultIsDateComplete, 8047 monthHeaderFormatter: this.monthHeaderFormatter || defaultMonthHeaderFormatter, 8048 weekNumberFormatter: this.weekNumberFormatter || defaultWeekNumberFormatter, 8049 longDateFormatter: this.longDateFormatter || defaultLongDateFormatter, 8050 msgCalendar: this.msgCalendar || defaultMsgCalendar, 8051 msgOpenCalendar: this.msgOpenCalendar || defaultMsgOpenCalendar 8052 }; 8053 8054 return service; 8055 }; 8056 DateLocaleProvider.prototype.$get.$inject = ["$locale"]; 8057 8058 $provide.provider('$mdDateLocale', new DateLocaleProvider()); 8059 }]); 8060 })(); 8061 8062 })(); 8063 (function(){ 8064 "use strict"; 8065 8066 (function() { 8067 'use strict'; 8068 8069 // POST RELEASE 8070 // TODO(jelbourn): Demo that uses moment.js 8071 // TODO(jelbourn): make sure this plays well with validation and ngMessages. 8072 // TODO(jelbourn): calendar pane doesn't open up outside of visible viewport. 8073 // TODO(jelbourn): forward more attributes to the internal input (required, autofocus, etc.) 8074 // TODO(jelbourn): something better for mobile (calendar panel takes up entire screen?) 8075 // TODO(jelbourn): input behavior (masking? auto-complete?) 8076 // TODO(jelbourn): UTC mode 8077 // TODO(jelbourn): RTL 8078 8079 8080 angular.module('material.components.datepicker') 8081 .directive('mdDatepicker', datePickerDirective); 8082 8083 /** 8084 * @ngdoc directive 8085 * @name mdDatepicker 8086 * @module material.components.datepicker 8087 * 8088 * @param {Date} ng-model The component's model. Expects a JavaScript Date object. 8089 * @param {expression=} ng-change Expression evaluated when the model value changes. 8090 * @param {Date=} md-min-date Expression representing a min date (inclusive). 8091 * @param {Date=} md-max-date Expression representing a max date (inclusive). 8092 * @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a boolean whether it can be selected or not. 8093 * @param {String=} md-placeholder The date input placeholder value. 8094 * @param {boolean=} ng-disabled Whether the datepicker is disabled. 8095 * @param {boolean=} ng-required Whether a value is required for the datepicker. 8096 * 8097 * @description 8098 * `<md-datepicker>` is a component used to select a single date. 8099 * For information on how to configure internationalization for the date picker, 8100 * see `$mdDateLocaleProvider`. 8101 * 8102 * This component supports [ngMessages](https://docs.angularjs.org/api/ngMessages/directive/ngMessages). 8103 * Supported attributes are: 8104 * * `required`: whether a required date is not set. 8105 * * `mindate`: whether the selected date is before the minimum allowed date. 8106 * * `maxdate`: whether the selected date is after the maximum allowed date. 8107 * 8108 * @usage 8109 * <hljs lang="html"> 8110 * <md-datepicker ng-model="birthday"></md-datepicker> 8111 * </hljs> 8112 * 8113 */ 8114 function datePickerDirective() { 8115 return { 8116 template: 8117 // Buttons are not in the tab order because users can open the calendar via keyboard 8118 // interaction on the text input, and multiple tab stops for one component (picker) 8119 // may be confusing. 8120 '<md-button class="md-datepicker-button md-icon-button" type="button" ' + 8121 'tabindex="-1" aria-hidden="true" ' + 8122 'ng-click="ctrl.openCalendarPane($event)">' + 8123 '<md-icon class="md-datepicker-calendar-icon" md-svg-icon="md-calendar"></md-icon>' + 8124 '</md-button>' + 8125 '<div class="md-datepicker-input-container" ' + 8126 'ng-class="{\'md-datepicker-focused\': ctrl.isFocused}">' + 8127 '<input class="md-datepicker-input" aria-haspopup="true" ' + 8128 'ng-focus="ctrl.setFocused(true)" ng-blur="ctrl.setFocused(false)">' + 8129 '<md-button type="button" md-no-ink ' + 8130 'class="md-datepicker-triangle-button md-icon-button" ' + 8131 'ng-click="ctrl.openCalendarPane($event)" ' + 8132 'aria-label="{{::ctrl.dateLocale.msgOpenCalendar}}">' + 8133 '<div class="md-datepicker-expand-triangle"></div>' + 8134 '</md-button>' + 8135 '</div>' + 8136 8137 // This pane will be detached from here and re-attached to the document body. 8138 '<div class="md-datepicker-calendar-pane md-whiteframe-z1">' + 8139 '<div class="md-datepicker-input-mask">' + 8140 '<div class="md-datepicker-input-mask-opaque"></div>' + 8141 '</div>' + 8142 '<div class="md-datepicker-calendar">' + 8143 '<md-calendar role="dialog" aria-label="{{::ctrl.dateLocale.msgCalendar}}" ' + 8144 'md-min-date="ctrl.minDate" md-max-date="ctrl.maxDate"' + 8145 'md-date-filter="ctrl.dateFilter"' + 8146 'ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen">' + 8147 '</md-calendar>' + 8148 '</div>' + 8149 '</div>', 8150 require: ['ngModel', 'mdDatepicker', '?^mdInputContainer'], 8151 scope: { 8152 minDate: '=mdMinDate', 8153 maxDate: '=mdMaxDate', 8154 placeholder: '@mdPlaceholder', 8155 dateFilter: '=mdDateFilter' 8156 }, 8157 controller: DatePickerCtrl, 8158 controllerAs: 'ctrl', 8159 bindToController: true, 8160 link: function(scope, element, attr, controllers) { 8161 var ngModelCtrl = controllers[0]; 8162 var mdDatePickerCtrl = controllers[1]; 8163 8164 var mdInputContainer = controllers[2]; 8165 if (mdInputContainer) { 8166 throw Error('md-datepicker should not be placed inside md-input-container.'); 8167 } 8168 8169 mdDatePickerCtrl.configureNgModel(ngModelCtrl); 8170 } 8171 }; 8172 } 8173 8174 /** Additional offset for the input's `size` attribute, which is updated based on its content. */ 8175 var EXTRA_INPUT_SIZE = 3; 8176 8177 /** Class applied to the container if the date is invalid. */ 8178 var INVALID_CLASS = 'md-datepicker-invalid'; 8179 8180 /** Default time in ms to debounce input event by. */ 8181 var DEFAULT_DEBOUNCE_INTERVAL = 500; 8182 8183 /** 8184 * Height of the calendar pane used to check if the pane is going outside the boundary of 8185 * the viewport. See calendar.scss for how $md-calendar-height is computed; an extra 20px is 8186 * also added to space the pane away from the exact edge of the screen. 8187 * 8188 * This is computed statically now, but can be changed to be measured if the circumstances 8189 * of calendar sizing are changed. 8190 */ 8191 var CALENDAR_PANE_HEIGHT = 368; 8192 8193 /** 8194 * Width of the calendar pane used to check if the pane is going outside the boundary of 8195 * the viewport. See calendar.scss for how $md-calendar-width is computed; an extra 20px is 8196 * also added to space the pane away from the exact edge of the screen. 8197 * 8198 * This is computed statically now, but can be changed to be measured if the circumstances 8199 * of calendar sizing are changed. 8200 */ 8201 var CALENDAR_PANE_WIDTH = 360; 8202 8203 /** 8204 * Controller for md-datepicker. 8205 * 8206 * @ngInject @constructor 8207 */ 8208 function DatePickerCtrl($scope, $element, $attrs, $compile, $timeout, $window, 8209 $mdConstant, $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF) { 8210 /** @final */ 8211 this.$compile = $compile; 8212 8213 /** @final */ 8214 this.$timeout = $timeout; 8215 8216 /** @final */ 8217 this.$window = $window; 8218 8219 /** @final */ 8220 this.dateLocale = $mdDateLocale; 8221 8222 /** @final */ 8223 this.dateUtil = $$mdDateUtil; 8224 8225 /** @final */ 8226 this.$mdConstant = $mdConstant; 8227 8228 /* @final */ 8229 this.$mdUtil = $mdUtil; 8230 8231 /** @final */ 8232 this.$$rAF = $$rAF; 8233 8234 /** 8235 * The root document element. This is used for attaching a top-level click handler to 8236 * close the calendar panel when a click outside said panel occurs. We use `documentElement` 8237 * instead of body because, when scrolling is disabled, some browsers consider the body element 8238 * to be completely off the screen and propagate events directly to the html element. 8239 * @type {!angular.JQLite} 8240 */ 8241 this.documentElement = angular.element(document.documentElement); 8242 8243 /** @type {!angular.NgModelController} */ 8244 this.ngModelCtrl = null; 8245 8246 /** @type {HTMLInputElement} */ 8247 this.inputElement = $element[0].querySelector('input'); 8248 8249 /** @final {!angular.JQLite} */ 8250 this.ngInputElement = angular.element(this.inputElement); 8251 8252 /** @type {HTMLElement} */ 8253 this.inputContainer = $element[0].querySelector('.md-datepicker-input-container'); 8254 8255 /** @type {HTMLElement} Floating calendar pane. */ 8256 this.calendarPane = $element[0].querySelector('.md-datepicker-calendar-pane'); 8257 8258 /** @type {HTMLElement} Calendar icon button. */ 8259 this.calendarButton = $element[0].querySelector('.md-datepicker-button'); 8260 8261 /** 8262 * Element covering everything but the input in the top of the floating calendar pane. 8263 * @type {HTMLElement} 8264 */ 8265 this.inputMask = $element[0].querySelector('.md-datepicker-input-mask-opaque'); 8266 8267 /** @final {!angular.JQLite} */ 8268 this.$element = $element; 8269 8270 /** @final {!angular.Attributes} */ 8271 this.$attrs = $attrs; 8272 8273 /** @final {!angular.Scope} */ 8274 this.$scope = $scope; 8275 8276 /** @type {Date} */ 8277 this.date = null; 8278 8279 /** @type {boolean} */ 8280 this.isFocused = false; 8281 8282 /** @type {boolean} */ 8283 this.isDisabled; 8284 this.setDisabled($element[0].disabled || angular.isString($attrs['disabled'])); 8285 8286 /** @type {boolean} Whether the date-picker's calendar pane is open. */ 8287 this.isCalendarOpen = false; 8288 8289 /** 8290 * Element from which the calendar pane was opened. Keep track of this so that we can return 8291 * focus to it when the pane is closed. 8292 * @type {HTMLElement} 8293 */ 8294 this.calendarPaneOpenedFrom = null; 8295 8296 this.calendarPane.id = 'md-date-pane' + $mdUtil.nextUid(); 8297 8298 $mdTheming($element); 8299 8300 /** Pre-bound click handler is saved so that the event listener can be removed. */ 8301 this.bodyClickHandler = angular.bind(this, this.handleBodyClick); 8302 8303 /** Pre-bound resize handler so that the event listener can be removed. */ 8304 this.windowResizeHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100); 8305 8306 // Unless the user specifies so, the datepicker should not be a tab stop. 8307 // This is necessary because ngAria might add a tabindex to anything with an ng-model 8308 // (based on whether or not the user has turned that particular feature on/off). 8309 if (!$attrs['tabindex']) { 8310 $element.attr('tabindex', '-1'); 8311 } 8312 8313 this.installPropertyInterceptors(); 8314 this.attachChangeListeners(); 8315 this.attachInteractionListeners(); 8316 8317 var self = this; 8318 $scope.$on('$destroy', function() { 8319 self.detachCalendarPane(); 8320 }); 8321 } 8322 DatePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$compile", "$timeout", "$window", "$mdConstant", "$mdTheming", "$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF"]; 8323 8324 /** 8325 * Sets up the controller's reference to ngModelController. 8326 * @param {!angular.NgModelController} ngModelCtrl 8327 */ 8328 DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl) { 8329 this.ngModelCtrl = ngModelCtrl; 8330 8331 var self = this; 8332 ngModelCtrl.$render = function() { 8333 var value = self.ngModelCtrl.$viewValue; 8334 8335 if (value && !(value instanceof Date)) { 8336 throw Error('The ng-model for md-datepicker must be a Date instance. ' + 8337 'Currently the model is a: ' + (typeof value)); 8338 } 8339 8340 self.date = value; 8341 self.inputElement.value = self.dateLocale.formatDate(value); 8342 self.resizeInputElement(); 8343 self.updateErrorState(); 8344 }; 8345 }; 8346 8347 /** 8348 * Attach event listeners for both the text input and the md-calendar. 8349 * Events are used instead of ng-model so that updates don't infinitely update the other 8350 * on a change. This should also be more performant than using a $watch. 8351 */ 8352 DatePickerCtrl.prototype.attachChangeListeners = function() { 8353 var self = this; 8354 8355 self.$scope.$on('md-calendar-change', function(event, date) { 8356 self.ngModelCtrl.$setViewValue(date); 8357 self.date = date; 8358 self.inputElement.value = self.dateLocale.formatDate(date); 8359 self.closeCalendarPane(); 8360 self.resizeInputElement(); 8361 self.updateErrorState(); 8362 }); 8363 8364 self.ngInputElement.on('input', angular.bind(self, self.resizeInputElement)); 8365 // TODO(chenmike): Add ability for users to specify this interval. 8366 self.ngInputElement.on('input', self.$mdUtil.debounce(self.handleInputEvent, 8367 DEFAULT_DEBOUNCE_INTERVAL, self)); 8368 }; 8369 8370 /** Attach event listeners for user interaction. */ 8371 DatePickerCtrl.prototype.attachInteractionListeners = function() { 8372 var self = this; 8373 var $scope = this.$scope; 8374 var keyCodes = this.$mdConstant.KEY_CODE; 8375 8376 // Add event listener through angular so that we can triggerHandler in unit tests. 8377 self.ngInputElement.on('keydown', function(event) { 8378 if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) { 8379 self.openCalendarPane(event); 8380 $scope.$digest(); 8381 } 8382 }); 8383 8384 $scope.$on('md-calendar-close', function() { 8385 self.closeCalendarPane(); 8386 }); 8387 }; 8388 8389 /** 8390 * Capture properties set to the date-picker and imperitively handle internal changes. 8391 * This is done to avoid setting up additional $watches. 8392 */ 8393 DatePickerCtrl.prototype.installPropertyInterceptors = function() { 8394 var self = this; 8395 8396 if (this.$attrs['ngDisabled']) { 8397 // The expression is to be evaluated against the directive element's scope and not 8398 // the directive's isolate scope. 8399 var scope = this.$scope.$parent; 8400 8401 if (scope) { 8402 scope.$watch(this.$attrs['ngDisabled'], function(isDisabled) { 8403 self.setDisabled(isDisabled); 8404 }); 8405 } 8406 } 8407 8408 Object.defineProperty(this, 'placeholder', { 8409 get: function() { return self.inputElement.placeholder; }, 8410 set: function(value) { self.inputElement.placeholder = value || ''; } 8411 }); 8412 }; 8413 8414 /** 8415 * Sets whether the date-picker is disabled. 8416 * @param {boolean} isDisabled 8417 */ 8418 DatePickerCtrl.prototype.setDisabled = function(isDisabled) { 8419 this.isDisabled = isDisabled; 8420 this.inputElement.disabled = isDisabled; 8421 this.calendarButton.disabled = isDisabled; 8422 }; 8423 8424 /** 8425 * Sets the custom ngModel.$error flags to be consumed by ngMessages. Flags are: 8426 * - mindate: whether the selected date is before the minimum date. 8427 * - maxdate: whether the selected flag is after the maximum date. 8428 * - filtered: whether the selected date is allowed by the custom filtering function. 8429 * - valid: whether the entered text input is a valid date 8430 * 8431 * The 'required' flag is handled automatically by ngModel. 8432 * 8433 * @param {Date=} opt_date Date to check. If not given, defaults to the datepicker's model value. 8434 */ 8435 DatePickerCtrl.prototype.updateErrorState = function(opt_date) { 8436 var date = opt_date || this.date; 8437 8438 // Clear any existing errors to get rid of anything that's no longer relevant. 8439 this.clearErrorState(); 8440 8441 if (this.dateUtil.isValidDate(date)) { 8442 // Force all dates to midnight in order to ignore the time portion. 8443 date = this.dateUtil.createDateAtMidnight(date); 8444 8445 if (this.dateUtil.isValidDate(this.minDate)) { 8446 var minDate = this.dateUtil.createDateAtMidnight(this.minDate); 8447 this.ngModelCtrl.$setValidity('mindate', date >= minDate); 8448 } 8449 8450 if (this.dateUtil.isValidDate(this.maxDate)) { 8451 var maxDate = this.dateUtil.createDateAtMidnight(this.maxDate); 8452 this.ngModelCtrl.$setValidity('maxdate', date <= maxDate); 8453 } 8454 8455 if (angular.isFunction(this.dateFilter)) { 8456 this.ngModelCtrl.$setValidity('filtered', this.dateFilter(date)); 8457 } 8458 } else { 8459 // The date is seen as "not a valid date" if there is *something* set 8460 // (i.e.., not null or undefined), but that something isn't a valid date. 8461 this.ngModelCtrl.$setValidity('valid', date == null); 8462 } 8463 8464 // TODO(jelbourn): Change this to classList.toggle when we stop using PhantomJS in unit tests 8465 // because it doesn't conform to the DOMTokenList spec. 8466 // See https://github.com/ariya/phantomjs/issues/12782. 8467 if (!this.ngModelCtrl.$valid) { 8468 this.inputContainer.classList.add(INVALID_CLASS); 8469 } 8470 }; 8471 8472 /** Clears any error flags set by `updateErrorState`. */ 8473 DatePickerCtrl.prototype.clearErrorState = function() { 8474 this.inputContainer.classList.remove(INVALID_CLASS); 8475 ['mindate', 'maxdate', 'filtered', 'valid'].forEach(function(field) { 8476 this.ngModelCtrl.$setValidity(field, true); 8477 }, this); 8478 }; 8479 8480 /** Resizes the input element based on the size of its content. */ 8481 DatePickerCtrl.prototype.resizeInputElement = function() { 8482 this.inputElement.size = this.inputElement.value.length + EXTRA_INPUT_SIZE; 8483 }; 8484 8485 /** 8486 * Sets the model value if the user input is a valid date. 8487 * Adds an invalid class to the input element if not. 8488 */ 8489 DatePickerCtrl.prototype.handleInputEvent = function() { 8490 var inputString = this.inputElement.value; 8491 var parsedDate = inputString ? this.dateLocale.parseDate(inputString) : null; 8492 this.dateUtil.setDateTimeToMidnight(parsedDate); 8493 8494 // An input string is valid if it is either empty (representing no date) 8495 // or if it parses to a valid date that the user is allowed to select. 8496 var isValidInput = inputString == '' || ( 8497 this.dateUtil.isValidDate(parsedDate) && 8498 this.dateLocale.isDateComplete(inputString) && 8499 this.isDateEnabled(parsedDate) 8500 ); 8501 8502 // The datepicker's model is only updated when there is a valid input. 8503 if (isValidInput) { 8504 this.ngModelCtrl.$setViewValue(parsedDate); 8505 this.date = parsedDate; 8506 } 8507 8508 this.updateErrorState(parsedDate); 8509 }; 8510 8511 /** 8512 * Check whether date is in range and enabled 8513 * @param {Date=} opt_date 8514 * @return {boolean} Whether the date is enabled. 8515 */ 8516 DatePickerCtrl.prototype.isDateEnabled = function(opt_date) { 8517 return this.dateUtil.isDateWithinRange(opt_date, this.minDate, this.maxDate) && 8518 (!angular.isFunction(this.dateFilter) || this.dateFilter(opt_date)); 8519 }; 8520 8521 /** Position and attach the floating calendar to the document. */ 8522 DatePickerCtrl.prototype.attachCalendarPane = function() { 8523 var calendarPane = this.calendarPane; 8524 calendarPane.style.transform = ''; 8525 this.$element.addClass('md-datepicker-open'); 8526 8527 var elementRect = this.inputContainer.getBoundingClientRect(); 8528 var bodyRect = document.body.getBoundingClientRect(); 8529 8530 // Check to see if the calendar pane would go off the screen. If so, adjust position 8531 // accordingly to keep it within the viewport. 8532 var paneTop = elementRect.top - bodyRect.top; 8533 var paneLeft = elementRect.left - bodyRect.left; 8534 8535 // If ng-material has disabled body scrolling (for example, if a dialog is open), 8536 // then it's possible that the already-scrolled body has a negative top/left. In this case, 8537 // we want to treat the "real" top as (0 - bodyRect.top). In a normal scrolling situation, 8538 // though, the top of the viewport should just be the body's scroll position. 8539 var viewportTop = (bodyRect.top < 0 && document.body.scrollTop == 0) ? 8540 -bodyRect.top : 8541 document.body.scrollTop; 8542 8543 var viewportLeft = (bodyRect.left < 0 && document.body.scrollLeft == 0) ? 8544 -bodyRect.left : 8545 document.body.scrollLeft; 8546 8547 var viewportBottom = viewportTop + this.$window.innerHeight; 8548 var viewportRight = viewportLeft + this.$window.innerWidth; 8549 8550 // If the right edge of the pane would be off the screen and shifting it left by the 8551 // difference would not go past the left edge of the screen. If the calendar pane is too 8552 // big to fit on the screen at all, move it to the left of the screen and scale the entire 8553 // element down to fit. 8554 if (paneLeft + CALENDAR_PANE_WIDTH > viewportRight) { 8555 if (viewportRight - CALENDAR_PANE_WIDTH > 0) { 8556 paneLeft = viewportRight - CALENDAR_PANE_WIDTH; 8557 } else { 8558 paneLeft = viewportLeft; 8559 var scale = this.$window.innerWidth / CALENDAR_PANE_WIDTH; 8560 calendarPane.style.transform = 'scale(' + scale + ')'; 8561 } 8562 8563 calendarPane.classList.add('md-datepicker-pos-adjusted'); 8564 } 8565 8566 // If the bottom edge of the pane would be off the screen and shifting it up by the 8567 // difference would not go past the top edge of the screen. 8568 if (paneTop + CALENDAR_PANE_HEIGHT > viewportBottom && 8569 viewportBottom - CALENDAR_PANE_HEIGHT > viewportTop) { 8570 paneTop = viewportBottom - CALENDAR_PANE_HEIGHT; 8571 calendarPane.classList.add('md-datepicker-pos-adjusted'); 8572 } 8573 8574 calendarPane.style.left = paneLeft + 'px'; 8575 calendarPane.style.top = paneTop + 'px'; 8576 document.body.appendChild(calendarPane); 8577 8578 // The top of the calendar pane is a transparent box that shows the text input underneath. 8579 // Since the pane is floating, though, the page underneath the pane *adjacent* to the input is 8580 // also shown unless we cover it up. The inputMask does this by filling up the remaining space 8581 // based on the width of the input. 8582 this.inputMask.style.left = elementRect.width + 'px'; 8583 8584 // Add CSS class after one frame to trigger open animation. 8585 this.$$rAF(function() { 8586 calendarPane.classList.add('md-pane-open'); 8587 }); 8588 }; 8589 8590 /** Detach the floating calendar pane from the document. */ 8591 DatePickerCtrl.prototype.detachCalendarPane = function() { 8592 this.$element.removeClass('md-datepicker-open'); 8593 this.calendarPane.classList.remove('md-pane-open'); 8594 this.calendarPane.classList.remove('md-datepicker-pos-adjusted'); 8595 8596 if (this.isCalendarOpen) { 8597 this.$mdUtil.enableScrolling(); 8598 } 8599 8600 if (this.calendarPane.parentNode) { 8601 // Use native DOM removal because we do not want any of the angular state of this element 8602 // to be disposed. 8603 this.calendarPane.parentNode.removeChild(this.calendarPane); 8604 } 8605 }; 8606 8607 /** 8608 * Open the floating calendar pane. 8609 * @param {Event} event 8610 */ 8611 DatePickerCtrl.prototype.openCalendarPane = function(event) { 8612 if (!this.isCalendarOpen && !this.isDisabled) { 8613 this.isCalendarOpen = true; 8614 this.calendarPaneOpenedFrom = event.target; 8615 8616 // Because the calendar pane is attached directly to the body, it is possible that the 8617 // rest of the component (input, etc) is in a different scrolling container, such as 8618 // an md-content. This means that, if the container is scrolled, the pane would remain 8619 // stationary. To remedy this, we disable scrolling while the calendar pane is open, which 8620 // also matches the native behavior for things like `<select>` on Mac and Windows. 8621 this.$mdUtil.disableScrollAround(this.calendarPane); 8622 8623 this.attachCalendarPane(); 8624 this.focusCalendar(); 8625 8626 // Attach click listener inside of a timeout because, if this open call was triggered by a 8627 // click, we don't want it to be immediately propogated up to the body and handled. 8628 var self = this; 8629 this.$mdUtil.nextTick(function() { 8630 // Use 'touchstart` in addition to click in order to work on iOS Safari, where click 8631 // events aren't propogated under most circumstances. 8632 // See http://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html 8633 self.documentElement.on('click touchstart', self.bodyClickHandler); 8634 }, false); 8635 8636 window.addEventListener('resize', this.windowResizeHandler); 8637 } 8638 }; 8639 8640 /** Close the floating calendar pane. */ 8641 DatePickerCtrl.prototype.closeCalendarPane = function() { 8642 if (this.isCalendarOpen) { 8643 this.detachCalendarPane(); 8644 this.isCalendarOpen = false; 8645 this.calendarPaneOpenedFrom.focus(); 8646 this.calendarPaneOpenedFrom = null; 8647 8648 this.ngModelCtrl.$setTouched(); 8649 8650 this.documentElement.off('click touchstart', this.bodyClickHandler); 8651 window.removeEventListener('resize', this.windowResizeHandler); 8652 } 8653 }; 8654 8655 /** Gets the controller instance for the calendar in the floating pane. */ 8656 DatePickerCtrl.prototype.getCalendarCtrl = function() { 8657 return angular.element(this.calendarPane.querySelector('md-calendar')).controller('mdCalendar'); 8658 }; 8659 8660 /** Focus the calendar in the floating pane. */ 8661 DatePickerCtrl.prototype.focusCalendar = function() { 8662 // Use a timeout in order to allow the calendar to be rendered, as it is gated behind an ng-if. 8663 var self = this; 8664 this.$mdUtil.nextTick(function() { 8665 self.getCalendarCtrl().focus(); 8666 }, false); 8667 }; 8668 8669 /** 8670 * Sets whether the input is currently focused. 8671 * @param {boolean} isFocused 8672 */ 8673 DatePickerCtrl.prototype.setFocused = function(isFocused) { 8674 if (!isFocused) { 8675 this.ngModelCtrl.$setTouched(); 8676 } 8677 this.isFocused = isFocused; 8678 }; 8679 8680 /** 8681 * Handles a click on the document body when the floating calendar pane is open. 8682 * Closes the floating calendar pane if the click is not inside of it. 8683 * @param {MouseEvent} event 8684 */ 8685 DatePickerCtrl.prototype.handleBodyClick = function(event) { 8686 if (this.isCalendarOpen) { 8687 // TODO(jelbourn): way want to also include the md-datepicker itself in this check. 8688 var isInCalendar = this.$mdUtil.getClosest(event.target, 'md-calendar'); 8689 if (!isInCalendar) { 8690 this.closeCalendarPane(); 8691 } 8692 8693 this.$scope.$digest(); 8694 } 8695 }; 8696 })(); 8697 8698 })(); 8699 (function(){ 8700 "use strict"; 8701 8702 (function() { 8703 'use strict'; 8704 8705 /** 8706 * Utility for performing date calculations to facilitate operation of the calendar and 8707 * datepicker. 8708 */ 8709 angular.module('material.components.datepicker').factory('$$mdDateUtil', function() { 8710 return { 8711 getFirstDateOfMonth: getFirstDateOfMonth, 8712 getNumberOfDaysInMonth: getNumberOfDaysInMonth, 8713 getDateInNextMonth: getDateInNextMonth, 8714 getDateInPreviousMonth: getDateInPreviousMonth, 8715 isInNextMonth: isInNextMonth, 8716 isInPreviousMonth: isInPreviousMonth, 8717 getDateMidpoint: getDateMidpoint, 8718 isSameMonthAndYear: isSameMonthAndYear, 8719 getWeekOfMonth: getWeekOfMonth, 8720 incrementDays: incrementDays, 8721 incrementMonths: incrementMonths, 8722 getLastDateOfMonth: getLastDateOfMonth, 8723 isSameDay: isSameDay, 8724 getMonthDistance: getMonthDistance, 8725 isValidDate: isValidDate, 8726 setDateTimeToMidnight: setDateTimeToMidnight, 8727 createDateAtMidnight: createDateAtMidnight, 8728 isDateWithinRange: isDateWithinRange 8729 }; 8730 8731 /** 8732 * Gets the first day of the month for the given date's month. 8733 * @param {Date} date 8734 * @returns {Date} 8735 */ 8736 function getFirstDateOfMonth(date) { 8737 return new Date(date.getFullYear(), date.getMonth(), 1); 8738 } 8739 8740 /** 8741 * Gets the number of days in the month for the given date's month. 8742 * @param date 8743 * @returns {number} 8744 */ 8745 function getNumberOfDaysInMonth(date) { 8746 return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); 8747 } 8748 8749 /** 8750 * Get an arbitrary date in the month after the given date's month. 8751 * @param date 8752 * @returns {Date} 8753 */ 8754 function getDateInNextMonth(date) { 8755 return new Date(date.getFullYear(), date.getMonth() + 1, 1); 8756 } 8757 8758 /** 8759 * Get an arbitrary date in the month before the given date's month. 8760 * @param date 8761 * @returns {Date} 8762 */ 8763 function getDateInPreviousMonth(date) { 8764 return new Date(date.getFullYear(), date.getMonth() - 1, 1); 8765 } 8766 8767 /** 8768 * Gets whether two dates have the same month and year. 8769 * @param {Date} d1 8770 * @param {Date} d2 8771 * @returns {boolean} 8772 */ 8773 function isSameMonthAndYear(d1, d2) { 8774 return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth(); 8775 } 8776 8777 /** 8778 * Gets whether two dates are the same day (not not necessarily the same time). 8779 * @param {Date} d1 8780 * @param {Date} d2 8781 * @returns {boolean} 8782 */ 8783 function isSameDay(d1, d2) { 8784 return d1.getDate() == d2.getDate() && isSameMonthAndYear(d1, d2); 8785 } 8786 8787 /** 8788 * Gets whether a date is in the month immediately after some date. 8789 * @param {Date} startDate The date from which to compare. 8790 * @param {Date} endDate The date to check. 8791 * @returns {boolean} 8792 */ 8793 function isInNextMonth(startDate, endDate) { 8794 var nextMonth = getDateInNextMonth(startDate); 8795 return isSameMonthAndYear(nextMonth, endDate); 8796 } 8797 8798 /** 8799 * Gets whether a date is in the month immediately before some date. 8800 * @param {Date} startDate The date from which to compare. 8801 * @param {Date} endDate The date to check. 8802 * @returns {boolean} 8803 */ 8804 function isInPreviousMonth(startDate, endDate) { 8805 var previousMonth = getDateInPreviousMonth(startDate); 8806 return isSameMonthAndYear(endDate, previousMonth); 8807 } 8808 8809 /** 8810 * Gets the midpoint between two dates. 8811 * @param {Date} d1 8812 * @param {Date} d2 8813 * @returns {Date} 8814 */ 8815 function getDateMidpoint(d1, d2) { 8816 return createDateAtMidnight((d1.getTime() + d2.getTime()) / 2); 8817 } 8818 8819 /** 8820 * Gets the week of the month that a given date occurs in. 8821 * @param {Date} date 8822 * @returns {number} Index of the week of the month (zero-based). 8823 */ 8824 function getWeekOfMonth(date) { 8825 var firstDayOfMonth = getFirstDateOfMonth(date); 8826 return Math.floor((firstDayOfMonth.getDay() + date.getDate() - 1) / 7); 8827 } 8828 8829 /** 8830 * Gets a new date incremented by the given number of days. Number of days can be negative. 8831 * @param {Date} date 8832 * @param {number} numberOfDays 8833 * @returns {Date} 8834 */ 8835 function incrementDays(date, numberOfDays) { 8836 return new Date(date.getFullYear(), date.getMonth(), date.getDate() + numberOfDays); 8837 } 8838 8839 /** 8840 * Gets a new date incremented by the given number of months. Number of months can be negative. 8841 * If the date of the given month does not match the target month, the date will be set to the 8842 * last day of the month. 8843 * @param {Date} date 8844 * @param {number} numberOfMonths 8845 * @returns {Date} 8846 */ 8847 function incrementMonths(date, numberOfMonths) { 8848 // If the same date in the target month does not actually exist, the Date object will 8849 // automatically advance *another* month by the number of missing days. 8850 // For example, if you try to go from Jan. 30 to Feb. 30, you'll end up on March 2. 8851 // So, we check if the month overflowed and go to the last day of the target month instead. 8852 var dateInTargetMonth = new Date(date.getFullYear(), date.getMonth() + numberOfMonths, 1); 8853 var numberOfDaysInMonth = getNumberOfDaysInMonth(dateInTargetMonth); 8854 if (numberOfDaysInMonth < date.getDate()) { 8855 dateInTargetMonth.setDate(numberOfDaysInMonth); 8856 } else { 8857 dateInTargetMonth.setDate(date.getDate()); 8858 } 8859 8860 return dateInTargetMonth; 8861 } 8862 8863 /** 8864 * Get the integer distance between two months. This *only* considers the month and year 8865 * portion of the Date instances. 8866 * 8867 * @param {Date} start 8868 * @param {Date} end 8869 * @returns {number} Number of months between `start` and `end`. If `end` is before `start` 8870 * chronologically, this number will be negative. 8871 */ 8872 function getMonthDistance(start, end) { 8873 return (12 * (end.getFullYear() - start.getFullYear())) + (end.getMonth() - start.getMonth()); 8874 } 8875 8876 /** 8877 * Gets the last day of the month for the given date. 8878 * @param {Date} date 8879 * @returns {Date} 8880 */ 8881 function getLastDateOfMonth(date) { 8882 return new Date(date.getFullYear(), date.getMonth(), getNumberOfDaysInMonth(date)); 8883 } 8884 8885 /** 8886 * Checks whether a date is valid. 8887 * @param {Date} date 8888 * @return {boolean} Whether the date is a valid Date. 8889 */ 8890 function isValidDate(date) { 8891 return date != null && date.getTime && !isNaN(date.getTime()); 8892 } 8893 8894 /** 8895 * Sets a date's time to midnight. 8896 * @param {Date} date 8897 */ 8898 function setDateTimeToMidnight(date) { 8899 if (isValidDate(date)) { 8900 date.setHours(0, 0, 0, 0); 8901 } 8902 } 8903 8904 /** 8905 * Creates a date with the time set to midnight. 8906 * Drop-in replacement for two forms of the Date constructor: 8907 * 1. No argument for Date representing now. 8908 * 2. Single-argument value representing number of seconds since Unix Epoch 8909 * or a Date object. 8910 * @param {number|Date=} opt_value 8911 * @return {Date} New date with time set to midnight. 8912 */ 8913 function createDateAtMidnight(opt_value) { 8914 var date; 8915 if (angular.isUndefined(opt_value)) { 8916 date = new Date(); 8917 } else { 8918 date = new Date(opt_value); 8919 } 8920 setDateTimeToMidnight(date); 8921 return date; 8922 } 8923 8924 /** 8925 * Checks if a date is within a min and max range, ignoring the time component. 8926 * If minDate or maxDate are not dates, they are ignored. 8927 * @param {Date} date 8928 * @param {Date} minDate 8929 * @param {Date} maxDate 8930 */ 8931 function isDateWithinRange(date, minDate, maxDate) { 8932 var dateAtMidnight = createDateAtMidnight(date); 8933 var minDateAtMidnight = isValidDate(minDate) ? createDateAtMidnight(minDate) : null; 8934 var maxDateAtMidnight = isValidDate(maxDate) ? createDateAtMidnight(maxDate) : null; 8935 return (!minDateAtMidnight || minDateAtMidnight <= dateAtMidnight) && 8936 (!maxDateAtMidnight || maxDateAtMidnight >= dateAtMidnight); 8937 } 8938 }); 8939 })(); 8940 8941 })(); 8942 (function(){ 8943 "use strict"; 8944 8945 /** 8946 * @ngdoc module 8947 * @name material.components.dialog 8948 */ 8949 angular 8950 .module('material.components.dialog', [ 8951 'material.core', 8952 'material.components.backdrop' 8953 ]) 8954 .directive('mdDialog', MdDialogDirective) 8955 .provider('$mdDialog', MdDialogProvider); 8956 8957 /** 8958 * @ngdoc directive 8959 * @name mdDialog 8960 * @module material.components.dialog 8961 * 8962 * @restrict E 8963 * 8964 * @description 8965 * `<md-dialog>` - The dialog's template must be inside this element. 8966 * 8967 * Inside, use an `<md-dialog-content>` element for the dialog's content, and use 8968 * an `<md-dialog-actions>` element for the dialog's actions. 8969 * 8970 * ## CSS 8971 * - `.md-dialog-content` - class that sets the padding on the content as the spec file 8972 * 8973 * ## Notes 8974 * - If you specify an `id` for the `<md-dialog>`, the `<md-dialog-content>` will have the same `id` 8975 * prefixed with `dialogContent_`. 8976 * 8977 * @usage 8978 * ### Dialog template 8979 * <hljs lang="html"> 8980 * <md-dialog aria-label="List dialog"> 8981 * <md-dialog-content> 8982 * <md-list> 8983 * <md-list-item ng-repeat="item in items"> 8984 * <p>Number {{item}}</p> 8985 * </md-list-item> 8986 * </md-list> 8987 * </md-dialog-content> 8988 * <md-dialog-actions> 8989 * <md-button ng-click="closeDialog()" class="md-primary">Close Dialog</md-button> 8990 * </md-dialog-actions> 8991 * </md-dialog> 8992 * </hljs> 8993 */ 8994 function MdDialogDirective($$rAF, $mdTheming, $mdDialog) { 8995 return { 8996 restrict: 'E', 8997 link: function(scope, element, attr) { 8998 $mdTheming(element); 8999 $$rAF(function() { 9000 var images; 9001 var content = element[0].querySelector('md-dialog-content'); 9002 9003 if (content) { 9004 images = content.getElementsByTagName('img'); 9005 addOverflowClass(); 9006 //-- delayed image loading may impact scroll height, check after images are loaded 9007 angular.element(images).on('load', addOverflowClass); 9008 } 9009 9010 scope.$on('$destroy', function() { 9011 $mdDialog.destroy(element); 9012 }); 9013 9014 /** 9015 * 9016 */ 9017 function addOverflowClass() { 9018 element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight); 9019 } 9020 9021 9022 }); 9023 } 9024 }; 9025 } 9026 MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"]; 9027 9028 /** 9029 * @ngdoc service 9030 * @name $mdDialog 9031 * @module material.components.dialog 9032 * 9033 * @description 9034 * `$mdDialog` opens a dialog over the app to inform users about critical information or require 9035 * them to make decisions. There are two approaches for setup: a simple promise API 9036 * and regular object syntax. 9037 * 9038 * ## Restrictions 9039 * 9040 * - The dialog is always given an isolate scope. 9041 * - The dialog's template must have an outer `<md-dialog>` element. 9042 * Inside, use an `<md-dialog-content>` element for the dialog's content, and use 9043 * an `<md-dialog-actions>` element for the dialog's actions. 9044 * - Dialogs must cover the entire application to keep interactions inside of them. 9045 * Use the `parent` option to change where dialogs are appended. 9046 * 9047 * ## Sizing 9048 * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`. 9049 * - Default max-width is 80% of the `rootElement` or `parent`. 9050 * 9051 * ## CSS 9052 * - `.md-dialog-content` - class that sets the padding on the content as the spec file 9053 * 9054 * @usage 9055 * <hljs lang="html"> 9056 * <div ng-app="demoApp" ng-controller="EmployeeController"> 9057 * <div> 9058 * <md-button ng-click="showAlert()" class="md-raised md-warn"> 9059 * Employee Alert! 9060 * </md-button> 9061 * </div> 9062 * <div> 9063 * <md-button ng-click="showDialog($event)" class="md-raised"> 9064 * Custom Dialog 9065 * </md-button> 9066 * </div> 9067 * <div> 9068 * <md-button ng-click="closeAlert()" ng-disabled="!hasAlert()" class="md-raised"> 9069 * Close Alert 9070 * </md-button> 9071 * </div> 9072 * <div> 9073 * <md-button ng-click="showGreeting($event)" class="md-raised md-primary" > 9074 * Greet Employee 9075 * </md-button> 9076 * </div> 9077 * </div> 9078 * </hljs> 9079 * 9080 * ### JavaScript: object syntax 9081 * <hljs lang="js"> 9082 * (function(angular, undefined){ 9083 * "use strict"; 9084 * 9085 * angular 9086 * .module('demoApp', ['ngMaterial']) 9087 * .controller('AppCtrl', AppController); 9088 * 9089 * function AppController($scope, $mdDialog) { 9090 * var alert; 9091 * $scope.showAlert = showAlert; 9092 * $scope.showDialog = showDialog; 9093 * $scope.items = [1, 2, 3]; 9094 * 9095 * // Internal method 9096 * function showAlert() { 9097 * alert = $mdDialog.alert({ 9098 * title: 'Attention', 9099 * textContent: 'This is an example of how easy dialogs can be!', 9100 * ok: 'Close' 9101 * }); 9102 * 9103 * $mdDialog 9104 * .show( alert ) 9105 * .finally(function() { 9106 * alert = undefined; 9107 * }); 9108 * } 9109 * 9110 * function showDialog($event) { 9111 * var parentEl = angular.element(document.body); 9112 * $mdDialog.show({ 9113 * parent: parentEl, 9114 * targetEvent: $event, 9115 * template: 9116 * '<md-dialog aria-label="List dialog">' + 9117 * ' <md-dialog-content>'+ 9118 * ' <md-list>'+ 9119 * ' <md-list-item ng-repeat="item in items">'+ 9120 * ' <p>Number {{item}}</p>' + 9121 * ' </md-item>'+ 9122 * ' </md-list>'+ 9123 * ' </md-dialog-content>' + 9124 * ' <md-dialog-actions>' + 9125 * ' <md-button ng-click="closeDialog()" class="md-primary">' + 9126 * ' Close Dialog' + 9127 * ' </md-button>' + 9128 * ' </md-dialog-actions>' + 9129 * '</md-dialog>', 9130 * locals: { 9131 * items: $scope.items 9132 * }, 9133 * controller: DialogController 9134 * }); 9135 * function DialogController($scope, $mdDialog, items) { 9136 * $scope.items = items; 9137 * $scope.closeDialog = function() { 9138 * $mdDialog.hide(); 9139 * } 9140 * } 9141 * } 9142 * } 9143 * })(angular); 9144 * </hljs> 9145 * 9146 * ### JavaScript: promise API syntax, custom dialog template 9147 * <hljs lang="js"> 9148 * (function(angular, undefined){ 9149 * "use strict"; 9150 * 9151 * angular 9152 * .module('demoApp', ['ngMaterial']) 9153 * .controller('EmployeeController', EmployeeEditor) 9154 * .controller('GreetingController', GreetingController); 9155 * 9156 * // Fictitious Employee Editor to show how to use simple and complex dialogs. 9157 * 9158 * function EmployeeEditor($scope, $mdDialog) { 9159 * var alert; 9160 * 9161 * $scope.showAlert = showAlert; 9162 * $scope.closeAlert = closeAlert; 9163 * $scope.showGreeting = showCustomGreeting; 9164 * 9165 * $scope.hasAlert = function() { return !!alert }; 9166 * $scope.userName = $scope.userName || 'Bobby'; 9167 * 9168 * // Dialog #1 - Show simple alert dialog and cache 9169 * // reference to dialog instance 9170 * 9171 * function showAlert() { 9172 * alert = $mdDialog.alert() 9173 * .title('Attention, ' + $scope.userName) 9174 * .textContent('This is an example of how easy dialogs can be!') 9175 * .ok('Close'); 9176 * 9177 * $mdDialog 9178 * .show( alert ) 9179 * .finally(function() { 9180 * alert = undefined; 9181 * }); 9182 * } 9183 * 9184 * // Close the specified dialog instance and resolve with 'finished' flag 9185 * // Normally this is not needed, just use '$mdDialog.hide()' to close 9186 * // the most recent dialog popup. 9187 * 9188 * function closeAlert() { 9189 * $mdDialog.hide( alert, "finished" ); 9190 * alert = undefined; 9191 * } 9192 * 9193 * // Dialog #2 - Demonstrate more complex dialogs construction and popup. 9194 * 9195 * function showCustomGreeting($event) { 9196 * $mdDialog.show({ 9197 * targetEvent: $event, 9198 * template: 9199 * '<md-dialog>' + 9200 * 9201 * ' <md-dialog-content>Hello {{ employee }}!</md-dialog-content>' + 9202 * 9203 * ' <md-dialog-actions>' + 9204 * ' <md-button ng-click="closeDialog()" class="md-primary">' + 9205 * ' Close Greeting' + 9206 * ' </md-button>' + 9207 * ' </md-dialog-actions>' + 9208 * '</md-dialog>', 9209 * controller: 'GreetingController', 9210 * onComplete: afterShowAnimation, 9211 * locals: { employee: $scope.userName } 9212 * }); 9213 * 9214 * // When the 'enter' animation finishes... 9215 * 9216 * function afterShowAnimation(scope, element, options) { 9217 * // post-show code here: DOM element focus, etc. 9218 * } 9219 * } 9220 * 9221 * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog 9222 * // Here we used ng-controller="GreetingController as vm" and 9223 * // $scope.vm === <controller instance> 9224 * 9225 * function showCustomGreeting() { 9226 * 9227 * $mdDialog.show({ 9228 * clickOutsideToClose: true, 9229 * 9230 * scope: $scope, // use parent scope in template 9231 * preserveScope: true, // do not forget this if use parent scope 9232 9233 * // Since GreetingController is instantiated with ControllerAs syntax 9234 * // AND we are passing the parent '$scope' to the dialog, we MUST 9235 * // use 'vm.<xxx>' in the template markup 9236 * 9237 * template: '<md-dialog>' + 9238 * ' <md-dialog-content>' + 9239 * ' Hi There {{vm.employee}}' + 9240 * ' </md-dialog-content>' + 9241 * '</md-dialog>', 9242 * 9243 * controller: function DialogController($scope, $mdDialog) { 9244 * $scope.closeDialog = function() { 9245 * $mdDialog.hide(); 9246 * } 9247 * } 9248 * }); 9249 * } 9250 * 9251 * } 9252 * 9253 * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog 9254 * 9255 * function GreetingController($scope, $mdDialog, employee) { 9256 * // Assigned from construction <code>locals</code> options... 9257 * $scope.employee = employee; 9258 * 9259 * $scope.closeDialog = function() { 9260 * // Easily hides most recent dialog shown... 9261 * // no specific instance reference is needed. 9262 * $mdDialog.hide(); 9263 * }; 9264 * } 9265 * 9266 * })(angular); 9267 * </hljs> 9268 */ 9269 9270 /** 9271 * @ngdoc method 9272 * @name $mdDialog#alert 9273 * 9274 * @description 9275 * Builds a preconfigured dialog with the specified message. 9276 * 9277 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: 9278 * 9279 * - $mdDialogPreset#title(string) - Sets the alert title. 9280 * - $mdDialogPreset#textContent(string) - Sets the alert message. 9281 * - $mdDialogPreset#htmlContent(string) - Sets the alert message as HTML. Requires ngSanitize 9282 * module to be loaded. HTML is not run through Angular's compiler. 9283 * - $mdDialogPreset#ok(string) - Sets the alert "Okay" button text. 9284 * - $mdDialogPreset#theme(string) - Sets the theme of the alert dialog. 9285 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, 9286 * the location of the click will be used as the starting point for the opening animation 9287 * of the the dialog. 9288 * 9289 */ 9290 9291 /** 9292 * @ngdoc method 9293 * @name $mdDialog#confirm 9294 * 9295 * @description 9296 * Builds a preconfigured dialog with the specified message. You can call show and the promise returned 9297 * will be resolved only if the user clicks the confirm action on the dialog. 9298 * 9299 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: 9300 * 9301 * Additionally, it supports the following methods: 9302 * 9303 * - $mdDialogPreset#title(string) - Sets the confirm title. 9304 * - $mdDialogPreset#textContent(string) - Sets the confirm message. 9305 * - $mdDialogPreset#htmlContent(string) - Sets the confirm message as HTML. Requires ngSanitize 9306 * module to be loaded. HTML is not run through Angular's compiler. 9307 * - $mdDialogPreset#ok(string) - Sets the confirm "Okay" button text. 9308 * - $mdDialogPreset#cancel(string) - Sets the confirm "Cancel" button text. 9309 * - $mdDialogPreset#theme(string) - Sets the theme of the confirm dialog. 9310 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, 9311 * the location of the click will be used as the starting point for the opening animation 9312 * of the the dialog. 9313 * 9314 */ 9315 9316 /** 9317 * @ngdoc method 9318 * @name $mdDialog#prompt 9319 * 9320 * @description 9321 * Builds a preconfigured dialog with the specified message and input box. You can call show and the promise returned 9322 * will be resolved only if the user clicks the prompt action on the dialog, passing the input value as the first argument. 9323 * 9324 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: 9325 * 9326 * Additionally, it supports the following methods: 9327 * 9328 * - $mdDialogPreset#title(string) - Sets the prompt title. 9329 * - $mdDialogPreset#textContent(string) - Sets the prompt message. 9330 * - $mdDialogPreset#htmlContent(string) - Sets the prompt message as HTML. Requires ngSanitize 9331 * module to be loaded. HTML is not run through Angular's compiler. 9332 * - $mdDialogPreset#placeholder(string) - Sets the placeholder text for the input. 9333 * - $mdDialogPreset#ok(string) - Sets the prompt "Okay" button text. 9334 * - $mdDialogPreset#cancel(string) - Sets the prompt "Cancel" button text. 9335 * - $mdDialogPreset#theme(string) - Sets the theme of the prompt dialog. 9336 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, 9337 * the location of the click will be used as the starting point for the opening animation 9338 * of the the dialog. 9339 * 9340 */ 9341 9342 /** 9343 * @ngdoc method 9344 * @name $mdDialog#show 9345 * 9346 * @description 9347 * Show a dialog with the specified options. 9348 * 9349 * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and 9350 * `confirm()`, or an options object with the following properties: 9351 * - `templateUrl` - `{string=}`: The url of a template that will be used as the content 9352 * of the dialog. 9353 * - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML 9354 * with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce). 9355 * This template should **never** be constructed with any kind of user input or user data. 9356 * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a 9357 * `<md-dialog>` tag if one is not provided. Defaults to true. Can be disabled if you provide a 9358 * custom dialog directive. 9359 * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option, 9360 * the location of the click will be used as the starting point for the opening animation 9361 * of the the dialog. 9362 * - `openFrom` - `{string|Element|object}`: The query selector, DOM element or the Rect object 9363 * that is used to determine the bounds (top, left, height, width) from which the Dialog will 9364 * originate. 9365 * - `closeTo` - `{string|Element|object}`: The query selector, DOM element or the Rect object 9366 * that is used to determine the bounds (top, left, height, width) to which the Dialog will 9367 * target. 9368 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, 9369 * it will create a new isolate scope. 9370 * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true. 9371 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false 9372 * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open. 9373 * Default true. 9374 * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog. 9375 * Default true. 9376 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to 9377 * close it. Default false. 9378 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog. 9379 * Default true. 9380 * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if 9381 * focusing some other way, as focus management is required for dialogs to be accessible. 9382 * Defaults to true. 9383 * - `controller` - `{function|string=}`: The controller to associate with the dialog. The controller 9384 * will be injected with the local `$mdDialog`, which passes along a scope for the dialog. 9385 * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names 9386 * of values to inject into the controller. For example, `locals: {three: 3}` would inject 9387 * `three` into the controller, with the value 3. If `bindToController` is true, they will be 9388 * copied to the controller instead. 9389 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. 9390 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the 9391 * dialog will not open until all of the promises resolve. 9392 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. 9393 * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending 9394 * to the root element of the application. 9395 * - `onShowing` `{function=} Callback function used to announce the show() action is 9396 * starting. 9397 * - `onComplete` `{function=}`: Callback function used to announce when the show() action is 9398 * finished. 9399 * - `onRemoving` `{function=}`: Callback function used to announce the close/hide() action is 9400 * starting. This allows developers to run custom animations in parallel the close animations. 9401 * - `fullscreen` `{boolean=}`: An option to apply `.md-dialog-fullscreen` class on open. 9402 * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or 9403 * rejected with `$mdDialog.cancel()`. 9404 */ 9405 9406 /** 9407 * @ngdoc method 9408 * @name $mdDialog#hide 9409 * 9410 * @description 9411 * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`. 9412 * 9413 * @param {*=} response An argument for the resolved promise. 9414 * 9415 * @returns {promise} A promise that is resolved when the dialog has been closed. 9416 */ 9417 9418 /** 9419 * @ngdoc method 9420 * @name $mdDialog#cancel 9421 * 9422 * @description 9423 * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`. 9424 * 9425 * @param {*=} response An argument for the rejected promise. 9426 * 9427 * @returns {promise} A promise that is resolved when the dialog has been closed. 9428 */ 9429 9430 function MdDialogProvider($$interimElementProvider) { 9431 // Elements to capture and redirect focus when the user presses tab at the dialog boundary. 9432 var topFocusTrap, bottomFocusTrap; 9433 9434 advancedDialogOptions.$inject = ["$mdDialog", "$mdTheming", "$mdConstant"]; 9435 dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector"]; 9436 return $$interimElementProvider('$mdDialog') 9437 .setDefaults({ 9438 methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 9439 'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen'], 9440 options: dialogDefaultOptions 9441 }) 9442 .addPreset('alert', { 9443 methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'theme', 9444 'css'], 9445 options: advancedDialogOptions 9446 }) 9447 .addPreset('confirm', { 9448 methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'cancel', 9449 'theme', 'css'], 9450 options: advancedDialogOptions 9451 }) 9452 .addPreset('prompt', { 9453 methods: ['title', 'htmlContent', 'textContent', 'content', 'placeholder', 'ariaLabel', 9454 'ok', 'cancel', 'theme', 'css'], 9455 options: advancedDialogOptions 9456 }); 9457 9458 /* @ngInject */ 9459 function advancedDialogOptions($mdDialog, $mdTheming, $mdConstant) { 9460 return { 9461 template: [ 9462 '<md-dialog md-theme="{{ dialog.theme }}" aria-label="{{ dialog.ariaLabel }}" ng-class="dialog.css">', 9463 ' <md-dialog-content class="md-dialog-content" role="document" tabIndex="-1">', 9464 ' <h2 class="md-title">{{ dialog.title }}</h2>', 9465 ' <div ng-if="::dialog.mdHtmlContent" class="md-dialog-content-body" ', 9466 ' ng-bind-html="::dialog.mdHtmlContent"></div>', 9467 ' <div ng-if="::!dialog.mdHtmlContent" class="md-dialog-content-body">', 9468 ' <p>{{::dialog.mdTextContent}}</p>', 9469 ' </div>', 9470 ' <md-input-container md-no-float ng-if="::dialog.$type == \'prompt\'" class="md-prompt-input-container">', 9471 ' <input ng-keypress="dialog.keypress($event)" md-autofocus ng-model="dialog.result" placeholder="{{::dialog.placeholder}}">', 9472 ' </md-input-container>', 9473 ' </md-dialog-content>', 9474 ' <md-dialog-actions>', 9475 ' <md-button ng-if="dialog.$type === \'confirm\' || dialog.$type === \'prompt\'"' + 9476 ' ng-click="dialog.abort()" class="md-primary">', 9477 ' {{ dialog.cancel }}', 9478 ' </md-button>', 9479 ' <md-button ng-click="dialog.hide()" class="md-primary" md-autofocus="dialog.$type===\'alert\'">', 9480 ' {{ dialog.ok }}', 9481 ' </md-button>', 9482 ' </md-dialog-actions>', 9483 '</md-dialog>' 9484 ].join('').replace(/\s\s+/g, ''), 9485 controller: function mdDialogCtrl() { 9486 this.hide = function() { 9487 $mdDialog.hide(this.$type === 'prompt' ? this.result : true); 9488 }; 9489 this.abort = function() { 9490 $mdDialog.cancel(); 9491 }; 9492 this.keypress = function($event) { 9493 if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) { 9494 $mdDialog.hide(this.result) 9495 } 9496 } 9497 }, 9498 controllerAs: 'dialog', 9499 bindToController: true, 9500 theme: $mdTheming.defaultTheme() 9501 }; 9502 } 9503 9504 /* @ngInject */ 9505 function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement, $log, $injector) { 9506 return { 9507 hasBackdrop: true, 9508 isolateScope: true, 9509 onShow: onShow, 9510 onShowing: beforeShow, 9511 onRemove: onRemove, 9512 clickOutsideToClose: false, 9513 escapeToClose: true, 9514 targetEvent: null, 9515 closeTo: null, 9516 openFrom: null, 9517 focusOnOpen: true, 9518 disableParentScroll: true, 9519 autoWrap: true, 9520 fullscreen: false, 9521 transformTemplate: function(template, options) { 9522 // Make the dialog container focusable, because otherwise the focus will be always redirected to 9523 // an element outside of the container, and the focus trap won't work probably.. 9524 // Also the tabindex is needed for the `escapeToClose` functionality, because 9525 // the keyDown event can't be triggered when the focus is outside of the container. 9526 return '<div class="md-dialog-container" tabindex="-1">' + validatedTemplate(template) + '</div>'; 9527 9528 /** 9529 * The specified template should contain a <md-dialog> wrapper element.... 9530 */ 9531 function validatedTemplate(template) { 9532 if (options.autoWrap && !/<\/md-dialog>/g.test(template)) { 9533 return '<md-dialog>' + (template || '') + '</md-dialog>'; 9534 } else { 9535 return template || ''; 9536 } 9537 } 9538 } 9539 }; 9540 9541 function beforeShow(scope, element, options, controller) { 9542 if (controller) { 9543 controller.mdHtmlContent = controller.htmlContent || options.htmlContent || ''; 9544 controller.mdTextContent = controller.textContent || options.textContent || 9545 controller.content || options.content || ''; 9546 9547 if (controller.mdHtmlContent && !$injector.has('$sanitize')) { 9548 throw Error('The ngSanitize module must be loaded in order to use htmlContent.'); 9549 } 9550 9551 if (controller.mdHtmlContent && controller.mdTextContent) { 9552 throw Error('md-dialog cannot have both `htmlContent` and `textContent`'); 9553 } 9554 } 9555 } 9556 9557 /** Show method for dialogs */ 9558 function onShow(scope, element, options, controller) { 9559 angular.element($document[0].body).addClass('md-dialog-is-showing'); 9560 9561 captureParentAndFromToElements(options); 9562 configureAria(element.find('md-dialog'), options); 9563 showBackdrop(scope, element, options); 9564 9565 return dialogPopIn(element, options) 9566 .then(function() { 9567 activateListeners(element, options); 9568 lockScreenReader(element, options); 9569 warnDeprecatedActions(); 9570 focusOnOpen(); 9571 }); 9572 9573 /** 9574 * Check to see if they used the deprecated .md-actions class and log a warning 9575 */ 9576 function warnDeprecatedActions() { 9577 var badActions = element[0].querySelectorAll('.md-actions'); 9578 9579 if (badActions.length > 0) { 9580 $log.warn('Using a class of md-actions is deprecated, please use <md-dialog-actions>.'); 9581 } 9582 } 9583 9584 /** 9585 * For alerts, focus on content... otherwise focus on 9586 * the close button (or equivalent) 9587 */ 9588 function focusOnOpen() { 9589 if (options.focusOnOpen) { 9590 var target = $mdUtil.findFocusTarget(element) || findCloseButton(); 9591 target.focus(); 9592 } 9593 9594 /** 9595 * If no element with class dialog-close, try to find the last 9596 * button child in md-actions and assume it is a close button. 9597 * 9598 * If we find no actions at all, log a warning to the console. 9599 */ 9600 function findCloseButton() { 9601 var closeButton = element[0].querySelector('.dialog-close'); 9602 if (!closeButton) { 9603 var actionButtons = element[0].querySelectorAll('.md-actions button, md-dialog-actions button'); 9604 closeButton = actionButtons[actionButtons.length - 1]; 9605 } 9606 return angular.element(closeButton); 9607 } 9608 } 9609 } 9610 9611 /** 9612 * Remove function for all dialogs 9613 */ 9614 function onRemove(scope, element, options) { 9615 options.deactivateListeners(); 9616 options.unlockScreenReader(); 9617 options.hideBackdrop(options.$destroy); 9618 9619 // Remove the focus traps that we added earlier for keeping focus within the dialog. 9620 if (topFocusTrap && topFocusTrap.parentNode) { 9621 topFocusTrap.parentNode.removeChild(topFocusTrap); 9622 } 9623 9624 if (bottomFocusTrap && bottomFocusTrap.parentNode) { 9625 bottomFocusTrap.parentNode.removeChild(bottomFocusTrap); 9626 } 9627 9628 // For navigation $destroy events, do a quick, non-animated removal, 9629 // but for normal closes (from clicks, etc) animate the removal 9630 return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean ); 9631 9632 /** 9633 * For normal closes, animate the removal. 9634 * For forced closes (like $destroy events), skip the animations 9635 */ 9636 function animateRemoval() { 9637 return dialogPopOut(element, options); 9638 } 9639 9640 /** 9641 * Detach the element 9642 */ 9643 function detachAndClean() { 9644 angular.element($document[0].body).removeClass('md-dialog-is-showing'); 9645 element.remove(); 9646 9647 if (!options.$destroy) options.origin.focus(); 9648 } 9649 } 9650 9651 /** 9652 * Capture originator/trigger/from/to element information (if available) 9653 * and the parent container for the dialog; defaults to the $rootElement 9654 * unless overridden in the options.parent 9655 */ 9656 function captureParentAndFromToElements(options) { 9657 options.origin = angular.extend({ 9658 element: null, 9659 bounds: null, 9660 focus: angular.noop 9661 }, options.origin || {}); 9662 9663 options.parent = getDomElement(options.parent, $rootElement); 9664 options.closeTo = getBoundingClientRect(getDomElement(options.closeTo)); 9665 options.openFrom = getBoundingClientRect(getDomElement(options.openFrom)); 9666 9667 if ( options.targetEvent ) { 9668 options.origin = getBoundingClientRect(options.targetEvent.target, options.origin); 9669 } 9670 9671 /** 9672 * Identify the bounding RECT for the target element 9673 * 9674 */ 9675 function getBoundingClientRect (element, orig) { 9676 var source = angular.element((element || {})); 9677 if (source && source.length) { 9678 // Compute and save the target element's bounding rect, so that if the 9679 // element is hidden when the dialog closes, we can shrink the dialog 9680 // back to the same position it expanded from. 9681 // 9682 // Checking if the source is a rect object or a DOM element 9683 var bounds = {top:0,left:0,height:0,width:0}; 9684 var hasFn = angular.isFunction(source[0].getBoundingClientRect); 9685 9686 return angular.extend(orig || {}, { 9687 element : hasFn ? source : undefined, 9688 bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]), 9689 focus : angular.bind(source, source.focus), 9690 }); 9691 } 9692 } 9693 9694 /** 9695 * If the specifier is a simple string selector, then query for 9696 * the DOM element. 9697 */ 9698 function getDomElement(element, defaultElement) { 9699 if (angular.isString(element)) { 9700 var simpleSelector = element, 9701 container = $document[0].querySelectorAll(simpleSelector); 9702 element = container.length ? container[0] : null; 9703 } 9704 9705 // If we have a reference to a raw dom element, always wrap it in jqLite 9706 return angular.element(element || defaultElement); 9707 } 9708 9709 } 9710 9711 /** 9712 * Listen for escape keys and outside clicks to auto close 9713 */ 9714 function activateListeners(element, options) { 9715 var window = angular.element($window); 9716 var onWindowResize = $mdUtil.debounce(function(){ 9717 stretchDialogContainerToViewport(element, options); 9718 }, 60); 9719 9720 var removeListeners = []; 9721 var smartClose = function() { 9722 // Only 'confirm' dialogs have a cancel button... escape/clickOutside will 9723 // cancel or fallback to hide. 9724 var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel; 9725 $mdUtil.nextTick(closeFn, true); 9726 }; 9727 9728 if (options.escapeToClose) { 9729 var parentTarget = options.parent; 9730 var keyHandlerFn = function(ev) { 9731 if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) { 9732 ev.stopPropagation(); 9733 ev.preventDefault(); 9734 9735 smartClose(); 9736 } 9737 }; 9738 9739 // Add keydown listeners 9740 element.on('keydown', keyHandlerFn); 9741 parentTarget.on('keydown', keyHandlerFn); 9742 9743 // Queue remove listeners function 9744 removeListeners.push(function() { 9745 9746 element.off('keydown', keyHandlerFn); 9747 parentTarget.off('keydown', keyHandlerFn); 9748 9749 }); 9750 } 9751 9752 // Register listener to update dialog on window resize 9753 window.on('resize', onWindowResize); 9754 9755 removeListeners.push(function() { 9756 window.off('resize', onWindowResize); 9757 }); 9758 9759 if (options.clickOutsideToClose) { 9760 var target = element; 9761 var sourceElem; 9762 9763 // Keep track of the element on which the mouse originally went down 9764 // so that we can only close the backdrop when the 'click' started on it. 9765 // A simple 'click' handler does not work, 9766 // it sets the target object as the element the mouse went down on. 9767 var mousedownHandler = function(ev) { 9768 sourceElem = ev.target; 9769 }; 9770 9771 // We check if our original element and the target is the backdrop 9772 // because if the original was the backdrop and the target was inside the dialog 9773 // we don't want to dialog to close. 9774 var mouseupHandler = function(ev) { 9775 if (sourceElem === target[0] && ev.target === target[0]) { 9776 ev.stopPropagation(); 9777 ev.preventDefault(); 9778 9779 smartClose(); 9780 } 9781 }; 9782 9783 // Add listeners 9784 target.on('mousedown', mousedownHandler); 9785 target.on('mouseup', mouseupHandler); 9786 9787 // Queue remove listeners function 9788 removeListeners.push(function() { 9789 target.off('mousedown', mousedownHandler); 9790 target.off('mouseup', mouseupHandler); 9791 }); 9792 } 9793 9794 // Attach specific `remove` listener handler 9795 options.deactivateListeners = function() { 9796 removeListeners.forEach(function(removeFn) { 9797 removeFn(); 9798 }); 9799 options.deactivateListeners = null; 9800 }; 9801 } 9802 9803 /** 9804 * Show modal backdrop element... 9805 */ 9806 function showBackdrop(scope, element, options) { 9807 9808 if (options.disableParentScroll) { 9809 // !! DO this before creating the backdrop; since disableScrollAround() 9810 // configures the scroll offset; which is used by mdBackDrop postLink() 9811 options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent); 9812 } 9813 9814 if (options.hasBackdrop) { 9815 options.backdrop = $mdUtil.createBackdrop(scope, "md-dialog-backdrop md-opaque"); 9816 $animate.enter(options.backdrop, options.parent); 9817 } 9818 9819 /** 9820 * Hide modal backdrop element... 9821 */ 9822 options.hideBackdrop = function hideBackdrop($destroy) { 9823 if (options.backdrop) { 9824 if ( !!$destroy ) options.backdrop.remove(); 9825 else $animate.leave(options.backdrop); 9826 } 9827 9828 if (options.disableParentScroll) { 9829 options.restoreScroll(); 9830 delete options.restoreScroll; 9831 } 9832 9833 options.hideBackdrop = null; 9834 } 9835 } 9836 9837 /** 9838 * Inject ARIA-specific attributes appropriate for Dialogs 9839 */ 9840 function configureAria(element, options) { 9841 9842 var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog'; 9843 var dialogContent = element.find('md-dialog-content'); 9844 var dialogContentId = 'dialogContent_' + (element.attr('id') || $mdUtil.nextUid()); 9845 9846 element.attr({ 9847 'role': role, 9848 'tabIndex': '-1' 9849 }); 9850 9851 if (dialogContent.length === 0) { 9852 dialogContent = element; 9853 } 9854 9855 dialogContent.attr('id', dialogContentId); 9856 element.attr('aria-describedby', dialogContentId); 9857 9858 if (options.ariaLabel) { 9859 $mdAria.expect(element, 'aria-label', options.ariaLabel); 9860 } 9861 else { 9862 $mdAria.expectAsync(element, 'aria-label', function() { 9863 var words = dialogContent.text().split(/\s+/); 9864 if (words.length > 3) words = words.slice(0, 3).concat('...'); 9865 return words.join(' '); 9866 }); 9867 } 9868 9869 // Set up elements before and after the dialog content to capture focus and 9870 // redirect back into the dialog. 9871 topFocusTrap = document.createElement('div'); 9872 topFocusTrap.classList.add('md-dialog-focus-trap'); 9873 topFocusTrap.tabIndex = 0; 9874 9875 bottomFocusTrap = topFocusTrap.cloneNode(false); 9876 9877 // When focus is about to move out of the dialog, we want to intercept it and redirect it 9878 // back to the dialog element. 9879 var focusHandler = function() { 9880 element.focus(); 9881 }; 9882 topFocusTrap.addEventListener('focus', focusHandler); 9883 bottomFocusTrap.addEventListener('focus', focusHandler); 9884 9885 // The top focus trap inserted immeidately before the md-dialog element (as a sibling). 9886 // The bottom focus trap is inserted at the very end of the md-dialog element (as a child). 9887 element[0].parentNode.insertBefore(topFocusTrap, element[0]); 9888 element.after(bottomFocusTrap); 9889 } 9890 9891 /** 9892 * Prevents screen reader interaction behind modal window 9893 * on swipe interfaces 9894 */ 9895 function lockScreenReader(element, options) { 9896 var isHidden = true; 9897 9898 // get raw DOM node 9899 walkDOM(element[0]); 9900 9901 options.unlockScreenReader = function() { 9902 isHidden = false; 9903 walkDOM(element[0]); 9904 9905 options.unlockScreenReader = null; 9906 }; 9907 9908 /** 9909 * Walk DOM to apply or remove aria-hidden on sibling nodes 9910 * and parent sibling nodes 9911 * 9912 */ 9913 function walkDOM(element) { 9914 while (element.parentNode) { 9915 if (element === document.body) { 9916 return; 9917 } 9918 var children = element.parentNode.children; 9919 for (var i = 0; i < children.length; i++) { 9920 // skip over child if it is an ascendant of the dialog 9921 // or a script or style tag 9922 if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) { 9923 children[i].setAttribute('aria-hidden', isHidden); 9924 } 9925 } 9926 9927 walkDOM(element = element.parentNode); 9928 } 9929 } 9930 } 9931 9932 /** 9933 * Ensure the dialog container fill-stretches to the viewport 9934 */ 9935 function stretchDialogContainerToViewport(container, options) { 9936 var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed'; 9937 var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null; 9938 var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0; 9939 9940 container.css({ 9941 top: (isFixed ? $mdUtil.scrollTop(options.parent) : 0) + 'px', 9942 height: height ? height + 'px' : '100%' 9943 }); 9944 9945 return container; 9946 } 9947 9948 /** 9949 * Dialog open and pop-in animation 9950 */ 9951 function dialogPopIn(container, options) { 9952 // Add the `md-dialog-container` to the DOM 9953 options.parent.append(container); 9954 stretchDialogContainerToViewport(container, options); 9955 9956 var dialogEl = container.find('md-dialog'); 9957 var animator = $mdUtil.dom.animator; 9958 var buildTranslateToOrigin = animator.calculateZoomToOrigin; 9959 var translateOptions = {transitionInClass: 'md-transition-in', transitionOutClass: 'md-transition-out'}; 9960 var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.openFrom || options.origin)); 9961 var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement) 9962 9963 if (options.fullscreen) { 9964 dialogEl.addClass('md-dialog-fullscreen'); 9965 } 9966 9967 return animator 9968 .translate3d(dialogEl, from, to, translateOptions) 9969 .then(function(animateReversal) { 9970 // Build a reversal translate function synched to this translation... 9971 options.reverseAnimate = function() { 9972 delete options.reverseAnimate; 9973 9974 if (options.closeTo) { 9975 // Using the opposite classes to create a close animation to the closeTo element 9976 translateOptions = {transitionInClass: 'md-transition-out', transitionOutClass: 'md-transition-in'}; 9977 from = to; 9978 to = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.closeTo)); 9979 9980 return animator 9981 .translate3d(dialogEl, from, to,translateOptions); 9982 } 9983 9984 return animateReversal( 9985 animator.toTransformCss( 9986 // in case the origin element has moved or is hidden, 9987 // let's recalculate the translateCSS 9988 buildTranslateToOrigin(dialogEl, options.origin) 9989 ) 9990 ); 9991 9992 }; 9993 return true; 9994 }); 9995 } 9996 9997 /** 9998 * Dialog close and pop-out animation 9999 */ 10000 function dialogPopOut(container, options) { 10001 return options.reverseAnimate(); 10002 } 10003 10004 /** 10005 * Utility function to filter out raw DOM nodes 10006 */ 10007 function isNodeOneOf(elem, nodeTypeArray) { 10008 if (nodeTypeArray.indexOf(elem.nodeName) !== -1) { 10009 return true; 10010 } 10011 } 10012 10013 } 10014 } 10015 MdDialogProvider.$inject = ["$$interimElementProvider"]; 10016 10017 })(); 10018 (function(){ 10019 "use strict"; 10020 10021 /** 10022 * @ngdoc module 10023 * @name material.components.divider 10024 * @description Divider module! 10025 */ 10026 angular.module('material.components.divider', [ 10027 'material.core' 10028 ]) 10029 .directive('mdDivider', MdDividerDirective); 10030 10031 /** 10032 * @ngdoc directive 10033 * @name mdDivider 10034 * @module material.components.divider 10035 * @restrict E 10036 * 10037 * @description 10038 * Dividers group and separate content within lists and page layouts using strong visual and spatial distinctions. This divider is a thin rule, lightweight enough to not distract the user from content. 10039 * 10040 * @param {boolean=} md-inset Add this attribute to activate the inset divider style. 10041 * @usage 10042 * <hljs lang="html"> 10043 * <md-divider></md-divider> 10044 * 10045 * <md-divider md-inset></md-divider> 10046 * </hljs> 10047 * 10048 */ 10049 function MdDividerDirective($mdTheming) { 10050 return { 10051 restrict: 'E', 10052 link: $mdTheming 10053 }; 10054 } 10055 MdDividerDirective.$inject = ["$mdTheming"]; 10056 10057 })(); 10058 (function(){ 10059 "use strict"; 10060 10061 (function() { 10062 'use strict'; 10063 10064 /** 10065 * @ngdoc module 10066 * @name material.components.fabActions 10067 */ 10068 angular 10069 .module('material.components.fabActions', ['material.core']) 10070 .directive('mdFabActions', MdFabActionsDirective); 10071 10072 /** 10073 * @ngdoc directive 10074 * @name mdFabActions 10075 * @module material.components.fabActions 10076 * 10077 * @restrict E 10078 * 10079 * @description 10080 * The `<md-fab-actions>` directive is used inside of a `<md-fab-speed-dial>` or 10081 * `<md-fab-toolbar>` directive to mark an element (or elements) as the actions and setup the 10082 * proper event listeners. 10083 * 10084 * @usage 10085 * See the `<md-fab-speed-dial>` or `<md-fab-toolbar>` directives for example usage. 10086 */ 10087 function MdFabActionsDirective() { 10088 return { 10089 restrict: 'E', 10090 10091 require: ['^?mdFabSpeedDial', '^?mdFabToolbar'], 10092 10093 compile: function(element, attributes) { 10094 var children = element.children(); 10095 10096 var hasNgRepeat = false; 10097 10098 angular.forEach(['', 'data-', 'x-'], function(prefix) { 10099 hasNgRepeat = hasNgRepeat || (children.attr(prefix + 'ng-repeat') ? true : false); 10100 }); 10101 10102 // Support both ng-repeat and static content 10103 if (hasNgRepeat) { 10104 children.addClass('md-fab-action-item'); 10105 } else { 10106 // Wrap every child in a new div and add a class that we can scale/fling independently 10107 children.wrap('<div class="md-fab-action-item">'); 10108 } 10109 } 10110 } 10111 } 10112 10113 })(); 10114 10115 })(); 10116 (function(){ 10117 "use strict"; 10118 10119 (function() { 10120 'use strict'; 10121 10122 angular.module('material.components.fabShared', ['material.core']) 10123 .controller('FabController', FabController); 10124 10125 function FabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) { 10126 var vm = this; 10127 10128 // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops 10129 10130 vm.open = function() { 10131 $scope.$evalAsync("vm.isOpen = true"); 10132 }; 10133 10134 vm.close = function() { 10135 // Async eval to avoid conflicts with existing digest loops 10136 $scope.$evalAsync("vm.isOpen = false"); 10137 10138 // Focus the trigger when the element closes so users can still tab to the next item 10139 $element.find('md-fab-trigger')[0].focus(); 10140 }; 10141 10142 // Toggle the open/close state when the trigger is clicked 10143 vm.toggle = function() { 10144 $scope.$evalAsync("vm.isOpen = !vm.isOpen"); 10145 }; 10146 10147 setupDefaults(); 10148 setupListeners(); 10149 setupWatchers(); 10150 10151 var initialAnimationAttempts = 0; 10152 fireInitialAnimations(); 10153 10154 function setupDefaults() { 10155 // Set the default direction to 'down' if none is specified 10156 vm.direction = vm.direction || 'down'; 10157 10158 // Set the default to be closed 10159 vm.isOpen = vm.isOpen || false; 10160 10161 // Start the keyboard interaction at the first action 10162 resetActionIndex(); 10163 10164 // Add an animations waiting class so we know not to run 10165 $element.addClass('md-animations-waiting'); 10166 } 10167 10168 function setupListeners() { 10169 var eventTypes = [ 10170 'click', 'focusin', 'focusout' 10171 ]; 10172 10173 // Add our listeners 10174 angular.forEach(eventTypes, function(eventType) { 10175 $element.on(eventType, parseEvents); 10176 }); 10177 10178 // Remove our listeners when destroyed 10179 $scope.$on('$destroy', function() { 10180 angular.forEach(eventTypes, function(eventType) { 10181 $element.off(eventType, parseEvents); 10182 }); 10183 10184 // remove any attached keyboard handlers in case element is removed while 10185 // speed dial is open 10186 disableKeyboard(); 10187 }); 10188 } 10189 10190 var closeTimeout; 10191 function parseEvents(event) { 10192 // If the event is a click, just handle it 10193 if (event.type == 'click') { 10194 handleItemClick(event); 10195 } 10196 10197 // If we focusout, set a timeout to close the element 10198 if (event.type == 'focusout' && !closeTimeout) { 10199 closeTimeout = $timeout(function() { 10200 vm.close(); 10201 }, 100, false); 10202 } 10203 10204 // If we see a focusin and there is a timeout about to run, cancel it so we stay open 10205 if (event.type == 'focusin' && closeTimeout) { 10206 $timeout.cancel(closeTimeout); 10207 closeTimeout = null; 10208 } 10209 } 10210 10211 function resetActionIndex() { 10212 vm.currentActionIndex = -1; 10213 } 10214 10215 function setupWatchers() { 10216 // Watch for changes to the direction and update classes/attributes 10217 $scope.$watch('vm.direction', function(newDir, oldDir) { 10218 // Add the appropriate classes so we can target the direction in the CSS 10219 $animate.removeClass($element, 'md-' + oldDir); 10220 $animate.addClass($element, 'md-' + newDir); 10221 10222 // Reset the action index since it may have changed 10223 resetActionIndex(); 10224 }); 10225 10226 var trigger, actions; 10227 10228 // Watch for changes to md-open 10229 $scope.$watch('vm.isOpen', function(isOpen) { 10230 // Reset the action index since it may have changed 10231 resetActionIndex(); 10232 10233 // We can't get the trigger/actions outside of the watch because the component hasn't been 10234 // linked yet, so we wait until the first watch fires to cache them. 10235 if (!trigger || !actions) { 10236 trigger = getTriggerElement(); 10237 actions = getActionsElement(); 10238 } 10239 10240 if (isOpen) { 10241 enableKeyboard(); 10242 } else { 10243 disableKeyboard(); 10244 } 10245 10246 var toAdd = isOpen ? 'md-is-open' : ''; 10247 var toRemove = isOpen ? '' : 'md-is-open'; 10248 10249 // Set the proper ARIA attributes 10250 trigger.attr('aria-haspopup', true); 10251 trigger.attr('aria-expanded', isOpen); 10252 actions.attr('aria-hidden', !isOpen); 10253 10254 // Animate the CSS classes 10255 $animate.setClass($element, toAdd, toRemove); 10256 }); 10257 } 10258 10259 function fireInitialAnimations() { 10260 // If the element is actually visible on the screen 10261 if ($element[0].scrollHeight > 0) { 10262 // Fire our animation 10263 $animate.addClass($element, 'md-animations-ready').then(function() { 10264 // Remove the waiting class 10265 $element.removeClass('md-animations-waiting'); 10266 }); 10267 } 10268 10269 // Otherwise, try for up to 1 second before giving up 10270 else if (initialAnimationAttempts < 10) { 10271 $timeout(fireInitialAnimations, 100); 10272 10273 // Increment our counter 10274 initialAnimationAttempts = initialAnimationAttempts + 1; 10275 } 10276 } 10277 10278 function enableKeyboard() { 10279 $element.on('keydown', keyPressed); 10280 10281 // On the next tick, setup a check for outside clicks; we do this on the next tick to avoid 10282 // clicks/touches that result in the isOpen attribute changing (e.g. a bound radio button) 10283 $mdUtil.nextTick(function() { 10284 angular.element(document).on('click touchend', checkForOutsideClick); 10285 }); 10286 10287 // TODO: On desktop, we should be able to reset the indexes so you cannot tab through, but 10288 // this breaks accessibility, especially on mobile, since you have no arrow keys to press 10289 //resetActionTabIndexes(); 10290 } 10291 10292 function disableKeyboard() { 10293 $element.off('keydown', keyPressed); 10294 angular.element(document).off('click touchend', checkForOutsideClick); 10295 } 10296 10297 function checkForOutsideClick(event) { 10298 if (event.target) { 10299 var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger'); 10300 var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions'); 10301 10302 if (!closestTrigger && !closestActions) { 10303 vm.close(); 10304 } 10305 } 10306 } 10307 10308 function keyPressed(event) { 10309 switch (event.which) { 10310 case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false; 10311 case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false; 10312 case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false; 10313 case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false; 10314 case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false; 10315 } 10316 } 10317 10318 function doActionPrev(event) { 10319 focusAction(event, -1); 10320 } 10321 10322 function doActionNext(event) { 10323 focusAction(event, 1); 10324 } 10325 10326 function focusAction(event, direction) { 10327 var actions = resetActionTabIndexes(); 10328 10329 // Increment/decrement the counter with restrictions 10330 vm.currentActionIndex = vm.currentActionIndex + direction; 10331 vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex); 10332 vm.currentActionIndex = Math.max(0, vm.currentActionIndex); 10333 10334 // Focus the element 10335 var focusElement = angular.element(actions[vm.currentActionIndex]).children()[0]; 10336 angular.element(focusElement).attr('tabindex', 0); 10337 focusElement.focus(); 10338 10339 // Make sure the event doesn't bubble and cause something else 10340 event.preventDefault(); 10341 event.stopImmediatePropagation(); 10342 } 10343 10344 function resetActionTabIndexes() { 10345 // Grab all of the actions 10346 var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item'); 10347 10348 // Disable all other actions for tabbing 10349 angular.forEach(actions, function(action) { 10350 angular.element(angular.element(action).children()[0]).attr('tabindex', -1); 10351 }); 10352 10353 return actions; 10354 } 10355 10356 function doKeyLeft(event) { 10357 if (vm.direction === 'left') { 10358 doActionNext(event); 10359 } else { 10360 doActionPrev(event); 10361 } 10362 } 10363 10364 function doKeyUp(event) { 10365 if (vm.direction === 'down') { 10366 doActionPrev(event); 10367 } else { 10368 doActionNext(event); 10369 } 10370 } 10371 10372 function doKeyRight(event) { 10373 if (vm.direction === 'left') { 10374 doActionPrev(event); 10375 } else { 10376 doActionNext(event); 10377 } 10378 } 10379 10380 function doKeyDown(event) { 10381 if (vm.direction === 'up') { 10382 doActionPrev(event); 10383 } else { 10384 doActionNext(event); 10385 } 10386 } 10387 10388 function isTrigger(element) { 10389 return $mdUtil.getClosest(element, 'md-fab-trigger'); 10390 } 10391 10392 function isAction(element) { 10393 return $mdUtil.getClosest(element, 'md-fab-actions'); 10394 } 10395 10396 function handleItemClick(event) { 10397 if (isTrigger(event.target)) { 10398 vm.toggle(); 10399 } 10400 10401 if (isAction(event.target)) { 10402 vm.close(); 10403 } 10404 } 10405 10406 function getTriggerElement() { 10407 return $element.find('md-fab-trigger'); 10408 } 10409 10410 function getActionsElement() { 10411 return $element.find('md-fab-actions'); 10412 } 10413 } 10414 FabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"]; 10415 })(); 10416 10417 })(); 10418 (function(){ 10419 "use strict"; 10420 10421 (function() { 10422 'use strict'; 10423 10424 /** 10425 * The duration of the CSS animation in milliseconds. 10426 * 10427 * @type {number} 10428 */ 10429 var cssAnimationDuration = 300; 10430 10431 /** 10432 * @ngdoc module 10433 * @name material.components.fabSpeedDial 10434 */ 10435 angular 10436 // Declare our module 10437 .module('material.components.fabSpeedDial', [ 10438 'material.core', 10439 'material.components.fabShared', 10440 'material.components.fabTrigger', 10441 'material.components.fabActions' 10442 ]) 10443 10444 // Register our directive 10445 .directive('mdFabSpeedDial', MdFabSpeedDialDirective) 10446 10447 // Register our custom animations 10448 .animation('.md-fling', MdFabSpeedDialFlingAnimation) 10449 .animation('.md-scale', MdFabSpeedDialScaleAnimation) 10450 10451 // Register a service for each animation so that we can easily inject them into unit tests 10452 .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation) 10453 .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation); 10454 10455 /** 10456 * @ngdoc directive 10457 * @name mdFabSpeedDial 10458 * @module material.components.fabSpeedDial 10459 * 10460 * @restrict E 10461 * 10462 * @description 10463 * The `<md-fab-speed-dial>` directive is used to present a series of popup elements (usually 10464 * `<md-button>`s) for quick access to common actions. 10465 * 10466 * There are currently two animations available by applying one of the following classes to 10467 * the component: 10468 * 10469 * - `md-fling` - The speed dial items appear from underneath the trigger and move into their 10470 * appropriate positions. 10471 * - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%. 10472 * 10473 * You may also easily position the trigger by applying one one of the following classes to the 10474 * `<md-fab-speed-dial>` element: 10475 * - `md-fab-top-left` 10476 * - `md-fab-top-right` 10477 * - `md-fab-bottom-left` 10478 * - `md-fab-bottom-right` 10479 * 10480 * These CSS classes use `position: absolute`, so you need to ensure that the container element 10481 * also uses `position: absolute` or `position: relative` in order for them to work. 10482 * 10483 * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to 10484 * open or close the speed dial. However, if you wish to allow users to hover over the empty 10485 * space where the actions will appear, you must also add the `md-hover-full` class to the speed 10486 * dial element. Without this, the hover effect will only occur on top of the trigger. 10487 * 10488 * See the demos for more information. 10489 * 10490 * ## Troubleshooting 10491 * 10492 * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on 10493 * the parent container to ensure that it is only visible once ready. We have plans to remove this 10494 * necessity in the future. 10495 * 10496 * @usage 10497 * <hljs lang="html"> 10498 * <md-fab-speed-dial md-direction="up" class="md-fling"> 10499 * <md-fab-trigger> 10500 * <md-button aria-label="Add..."><md-icon icon="/img/icons/plus.svg"></md-icon></md-button> 10501 * </md-fab-trigger> 10502 * 10503 * <md-fab-actions> 10504 * <md-button aria-label="Add User"> 10505 * <md-icon icon="/img/icons/user.svg"></md-icon> 10506 * </md-button> 10507 * 10508 * <md-button aria-label="Add Group"> 10509 * <md-icon icon="/img/icons/group.svg"></md-icon> 10510 * </md-button> 10511 * </md-fab-actions> 10512 * </md-fab-speed-dial> 10513 * </hljs> 10514 * 10515 * @param {string} md-direction From which direction you would like the speed dial to appear 10516 * relative to the trigger element. 10517 * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible. 10518 */ 10519 function MdFabSpeedDialDirective() { 10520 return { 10521 restrict: 'E', 10522 10523 scope: { 10524 direction: '@?mdDirection', 10525 isOpen: '=?mdOpen' 10526 }, 10527 10528 bindToController: true, 10529 controller: 'FabController', 10530 controllerAs: 'vm', 10531 10532 link: FabSpeedDialLink 10533 }; 10534 10535 function FabSpeedDialLink(scope, element) { 10536 // Prepend an element to hold our CSS variables so we can use them in the animations below 10537 element.prepend('<div class="md-css-variables"></div>'); 10538 } 10539 } 10540 10541 function MdFabSpeedDialFlingAnimation($timeout) { 10542 function delayDone(done) { $timeout(done, cssAnimationDuration, false); } 10543 10544 function runAnimation(element) { 10545 // Don't run if we are still waiting and we are not ready 10546 if (element.hasClass('md-animations-waiting') && !element.hasClass('md-animations-ready')) { 10547 return; 10548 } 10549 10550 var el = element[0]; 10551 var ctrl = element.controller('mdFabSpeedDial'); 10552 var items = el.querySelectorAll('.md-fab-action-item'); 10553 10554 // Grab our trigger element 10555 var triggerElement = el.querySelector('md-fab-trigger'); 10556 10557 // Grab our element which stores CSS variables 10558 var variablesElement = el.querySelector('.md-css-variables'); 10559 10560 // Setup JS variables based on our CSS variables 10561 var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); 10562 10563 // Always reset the items to their natural position/state 10564 angular.forEach(items, function(item, index) { 10565 var styles = item.style; 10566 10567 styles.transform = styles.webkitTransform = ''; 10568 styles.transitionDelay = ''; 10569 styles.opacity = 1; 10570 10571 // Make the items closest to the trigger have the highest z-index 10572 styles.zIndex = (items.length - index) + startZIndex; 10573 }); 10574 10575 // Set the trigger to be above all of the actions so they disappear behind it. 10576 triggerElement.style.zIndex = startZIndex + items.length + 1; 10577 10578 // If the control is closed, hide the items behind the trigger 10579 if (!ctrl.isOpen) { 10580 angular.forEach(items, function(item, index) { 10581 var newPosition, axis; 10582 var styles = item.style; 10583 10584 // Make sure to account for differences in the dimensions of the trigger verses the items 10585 // so that we can properly center everything; this helps hide the item's shadows behind 10586 // the trigger. 10587 var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2; 10588 var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2; 10589 10590 switch (ctrl.direction) { 10591 case 'up': 10592 newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset); 10593 axis = 'Y'; 10594 break; 10595 case 'down': 10596 newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset); 10597 axis = 'Y'; 10598 break; 10599 case 'left': 10600 newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset); 10601 axis = 'X'; 10602 break; 10603 case 'right': 10604 newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset); 10605 axis = 'X'; 10606 break; 10607 } 10608 10609 var newTranslate = 'translate' + axis + '(' + newPosition + 'px)'; 10610 10611 styles.transform = styles.webkitTransform = newTranslate; 10612 }); 10613 } 10614 } 10615 10616 return { 10617 addClass: function(element, className, done) { 10618 if (element.hasClass('md-fling')) { 10619 runAnimation(element); 10620 delayDone(done); 10621 } else { 10622 done(); 10623 } 10624 }, 10625 removeClass: function(element, className, done) { 10626 runAnimation(element); 10627 delayDone(done); 10628 } 10629 } 10630 } 10631 MdFabSpeedDialFlingAnimation.$inject = ["$timeout"]; 10632 10633 function MdFabSpeedDialScaleAnimation($timeout) { 10634 function delayDone(done) { $timeout(done, cssAnimationDuration, false); } 10635 10636 var delay = 65; 10637 10638 function runAnimation(element) { 10639 var el = element[0]; 10640 var ctrl = element.controller('mdFabSpeedDial'); 10641 var items = el.querySelectorAll('.md-fab-action-item'); 10642 10643 // Grab our element which stores CSS variables 10644 var variablesElement = el.querySelector('.md-css-variables'); 10645 10646 // Setup JS variables based on our CSS variables 10647 var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); 10648 10649 // Always reset the items to their natural position/state 10650 angular.forEach(items, function(item, index) { 10651 var styles = item.style, 10652 offsetDelay = index * delay; 10653 10654 styles.opacity = ctrl.isOpen ? 1 : 0; 10655 styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)'; 10656 styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms'; 10657 10658 // Make the items closest to the trigger have the highest z-index 10659 styles.zIndex = (items.length - index) + startZIndex; 10660 }); 10661 } 10662 10663 return { 10664 addClass: function(element, className, done) { 10665 runAnimation(element); 10666 delayDone(done); 10667 }, 10668 10669 removeClass: function(element, className, done) { 10670 runAnimation(element); 10671 delayDone(done); 10672 } 10673 } 10674 } 10675 MdFabSpeedDialScaleAnimation.$inject = ["$timeout"]; 10676 })(); 10677 10678 })(); 10679 (function(){ 10680 "use strict"; 10681 10682 (function() { 10683 'use strict'; 10684 10685 /** 10686 * @ngdoc module 10687 * @name material.components.fabToolbar 10688 */ 10689 angular 10690 // Declare our module 10691 .module('material.components.fabToolbar', [ 10692 'material.core', 10693 'material.components.fabShared', 10694 'material.components.fabTrigger', 10695 'material.components.fabActions' 10696 ]) 10697 10698 // Register our directive 10699 .directive('mdFabToolbar', MdFabToolbarDirective) 10700 10701 // Register our custom animations 10702 .animation('.md-fab-toolbar', MdFabToolbarAnimation) 10703 10704 // Register a service for the animation so that we can easily inject it into unit tests 10705 .service('mdFabToolbarAnimation', MdFabToolbarAnimation); 10706 10707 /** 10708 * @ngdoc directive 10709 * @name mdFabToolbar 10710 * @module material.components.fabToolbar 10711 * 10712 * @restrict E 10713 * 10714 * @description 10715 * 10716 * The `<md-fab-toolbar>` directive is used present a toolbar of elements (usually `<md-button>`s) 10717 * for quick access to common actions when a floating action button is activated (via click or 10718 * keyboard navigation). 10719 * 10720 * You may also easily position the trigger by applying one one of the following classes to the 10721 * `<md-fab-toolbar>` element: 10722 * - `md-fab-top-left` 10723 * - `md-fab-top-right` 10724 * - `md-fab-bottom-left` 10725 * - `md-fab-bottom-right` 10726 * 10727 * These CSS classes use `position: absolute`, so you need to ensure that the container element 10728 * also uses `position: absolute` or `position: relative` in order for them to work. 10729 * 10730 * @usage 10731 * 10732 * <hljs lang="html"> 10733 * <md-fab-toolbar md-direction='left'> 10734 * <md-fab-trigger> 10735 * <md-button aria-label="Add..."><md-icon icon="/img/icons/plus.svg"></md-icon></md-button> 10736 * </md-fab-trigger> 10737 * 10738 * <md-fab-actions> 10739 * <md-button aria-label="Add User"> 10740 * <md-icon icon="/img/icons/user.svg"></md-icon> 10741 * </md-button> 10742 * 10743 * <md-button aria-label="Add Group"> 10744 * <md-icon icon="/img/icons/group.svg"></md-icon> 10745 * </md-button> 10746 * </md-fab-actions> 10747 * </md-fab-toolbar> 10748 * </hljs> 10749 * 10750 * @param {string} md-direction From which direction you would like the toolbar items to appear 10751 * relative to the trigger element. Supports `left` and `right` directions. 10752 * @param {expression=} md-open Programmatically control whether or not the toolbar is visible. 10753 */ 10754 function MdFabToolbarDirective() { 10755 return { 10756 restrict: 'E', 10757 transclude: true, 10758 template: '<div class="md-fab-toolbar-wrapper">' + 10759 ' <div class="md-fab-toolbar-content" ng-transclude></div>' + 10760 '</div>', 10761 10762 scope: { 10763 direction: '@?mdDirection', 10764 isOpen: '=?mdOpen' 10765 }, 10766 10767 bindToController: true, 10768 controller: 'FabController', 10769 controllerAs: 'vm', 10770 10771 link: link 10772 }; 10773 10774 function link(scope, element, attributes) { 10775 // Add the base class for animations 10776 element.addClass('md-fab-toolbar'); 10777 10778 // Prepend the background element to the trigger's button 10779 element.find('md-fab-trigger').find('button') 10780 .prepend('<div class="md-fab-toolbar-background"></div>'); 10781 } 10782 } 10783 10784 function MdFabToolbarAnimation() { 10785 10786 function runAnimation(element, className, done) { 10787 // If no className was specified, don't do anything 10788 if (!className) { 10789 return; 10790 } 10791 10792 var el = element[0]; 10793 var ctrl = element.controller('mdFabToolbar'); 10794 10795 // Grab the relevant child elements 10796 var backgroundElement = el.querySelector('.md-fab-toolbar-background'); 10797 var triggerElement = el.querySelector('md-fab-trigger button'); 10798 var toolbarElement = el.querySelector('md-toolbar'); 10799 var iconElement = el.querySelector('md-fab-trigger button md-icon'); 10800 var actions = element.find('md-fab-actions').children(); 10801 10802 // If we have both elements, use them to position the new background 10803 if (triggerElement && backgroundElement) { 10804 // Get our variables 10805 var color = window.getComputedStyle(triggerElement).getPropertyValue('background-color'); 10806 var width = el.offsetWidth; 10807 var height = el.offsetHeight; 10808 10809 // Make it twice as big as it should be since we scale from the center 10810 var scale = 2 * (width / triggerElement.offsetWidth); 10811 10812 // Set some basic styles no matter what animation we're doing 10813 backgroundElement.style.backgroundColor = color; 10814 backgroundElement.style.borderRadius = width + 'px'; 10815 10816 // If we're open 10817 if (ctrl.isOpen) { 10818 // Turn on toolbar pointer events when closed 10819 toolbarElement.style.pointerEvents = 'initial'; 10820 10821 backgroundElement.style.width = triggerElement.offsetWidth + 'px'; 10822 backgroundElement.style.height = triggerElement.offsetHeight + 'px'; 10823 backgroundElement.style.transform = 'scale(' + scale + ')'; 10824 10825 // Set the next close animation to have the proper delays 10826 backgroundElement.style.transitionDelay = '0ms'; 10827 iconElement && (iconElement.style.transitionDelay = '.3s'); 10828 10829 // Apply a transition delay to actions 10830 angular.forEach(actions, function(action, index) { 10831 action.style.transitionDelay = (actions.length - index) * 25 + 'ms'; 10832 }); 10833 } else { 10834 // Turn off toolbar pointer events when closed 10835 toolbarElement.style.pointerEvents = 'none'; 10836 10837 // Scale it back down to the trigger's size 10838 backgroundElement.style.transform = 'scale(1)'; 10839 10840 // Reset the position 10841 backgroundElement.style.top = '0'; 10842 10843 if (element.hasClass('md-right')) { 10844 backgroundElement.style.left = '0'; 10845 backgroundElement.style.right = null; 10846 } 10847 10848 if (element.hasClass('md-left')) { 10849 backgroundElement.style.right = '0'; 10850 backgroundElement.style.left = null; 10851 } 10852 10853 // Set the next open animation to have the proper delays 10854 backgroundElement.style.transitionDelay = '200ms'; 10855 iconElement && (iconElement.style.transitionDelay = '0ms'); 10856 10857 // Apply a transition delay to actions 10858 angular.forEach(actions, function(action, index) { 10859 action.style.transitionDelay = 200 + (index * 25) + 'ms'; 10860 }); 10861 } 10862 } 10863 } 10864 10865 return { 10866 addClass: function(element, className, done) { 10867 runAnimation(element, className, done); 10868 done(); 10869 }, 10870 10871 removeClass: function(element, className, done) { 10872 runAnimation(element, className, done); 10873 done(); 10874 } 10875 } 10876 } 10877 })(); 10878 })(); 10879 (function(){ 10880 "use strict"; 10881 10882 (function() { 10883 'use strict'; 10884 10885 /** 10886 * @ngdoc module 10887 * @name material.components.fabTrigger 10888 */ 10889 angular 10890 .module('material.components.fabTrigger', ['material.core']) 10891 .directive('mdFabTrigger', MdFabTriggerDirective); 10892 10893 /** 10894 * @ngdoc directive 10895 * @name mdFabTrigger 10896 * @module material.components.fabSpeedDial 10897 * 10898 * @restrict E 10899 * 10900 * @description 10901 * The `<md-fab-trigger>` directive is used inside of a `<md-fab-speed-dial>` or 10902 * `<md-fab-toolbar>` directive to mark an element (or elements) as the trigger and setup the 10903 * proper event listeners. 10904 * 10905 * @usage 10906 * See the `<md-fab-speed-dial>` or `<md-fab-toolbar>` directives for example usage. 10907 */ 10908 function MdFabTriggerDirective() { 10909 // TODO: Remove this completely? 10910 return { 10911 restrict: 'E', 10912 10913 require: ['^?mdFabSpeedDial', '^?mdFabToolbar'] 10914 }; 10915 } 10916 })(); 10917 10918 10919 })(); 10920 (function(){ 10921 "use strict"; 10922 10923 /** 10924 * @ngdoc module 10925 * @name material.components.gridList 10926 */ 10927 angular.module('material.components.gridList', ['material.core']) 10928 .directive('mdGridList', GridListDirective) 10929 .directive('mdGridTile', GridTileDirective) 10930 .directive('mdGridTileFooter', GridTileCaptionDirective) 10931 .directive('mdGridTileHeader', GridTileCaptionDirective) 10932 .factory('$mdGridLayout', GridLayoutFactory); 10933 10934 /** 10935 * @ngdoc directive 10936 * @name mdGridList 10937 * @module material.components.gridList 10938 * @restrict E 10939 * @description 10940 * Grid lists are an alternative to standard list views. Grid lists are distinct 10941 * from grids used for layouts and other visual presentations. 10942 * 10943 * A grid list is best suited to presenting a homogenous data type, typically 10944 * images, and is optimized for visual comprehension and differentiating between 10945 * like data types. 10946 * 10947 * A grid list is a continuous element consisting of tessellated, regular 10948 * subdivisions called cells that contain tiles (`md-grid-tile`). 10949 * 10950 * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7OVlEaXZ5YmU1Xzg/components_grids_usage2.png" 10951 * style="width: 300px; height: auto; margin-right: 16px;" alt="Concept of grid explained visually"> 10952 * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7VGhsOE5idWlJWXM/components_grids_usage3.png" 10953 * style="width: 300px; height: auto;" alt="Grid concepts legend"> 10954 * 10955 * Cells are arrayed vertically and horizontally within the grid. 10956 * 10957 * Tiles hold content and can span one or more cells vertically or horizontally. 10958 * 10959 * ### Responsive Attributes 10960 * 10961 * The `md-grid-list` directive supports "responsive" attributes, which allow 10962 * different `md-cols`, `md-gutter` and `md-row-height` values depending on the 10963 * currently matching media query. 10964 * 10965 * In order to set a responsive attribute, first define the fallback value with 10966 * the standard attribute name, then add additional attributes with the 10967 * following convention: `{base-attribute-name}-{media-query-name}="{value}"` 10968 * (ie. `md-cols-lg="8"`) 10969 * 10970 * @param {number} md-cols Number of columns in the grid. 10971 * @param {string} md-row-height One of 10972 * <ul> 10973 * <li>CSS length - Fixed height rows (eg. `8px` or `1rem`)</li> 10974 * <li>`{width}:{height}` - Ratio of width to height (eg. 10975 * `md-row-height="16:9"`)</li> 10976 * <li>`"fit"` - Height will be determined by subdividing the available 10977 * height by the number of rows</li> 10978 * </ul> 10979 * @param {string=} md-gutter The amount of space between tiles in CSS units 10980 * (default 1px) 10981 * @param {expression=} md-on-layout Expression to evaluate after layout. Event 10982 * object is available as `$event`, and contains performance information. 10983 * 10984 * @usage 10985 * Basic: 10986 * <hljs lang="html"> 10987 * <md-grid-list md-cols="5" md-gutter="1em" md-row-height="4:3"> 10988 * <md-grid-tile></md-grid-tile> 10989 * </md-grid-list> 10990 * </hljs> 10991 * 10992 * Fixed-height rows: 10993 * <hljs lang="html"> 10994 * <md-grid-list md-cols="4" md-row-height="200px" ...> 10995 * <md-grid-tile></md-grid-tile> 10996 * </md-grid-list> 10997 * </hljs> 10998 * 10999 * Fit rows: 11000 * <hljs lang="html"> 11001 * <md-grid-list md-cols="4" md-row-height="fit" style="height: 400px;" ...> 11002 * <md-grid-tile></md-grid-tile> 11003 * </md-grid-list> 11004 * </hljs> 11005 * 11006 * Using responsive attributes: 11007 * <hljs lang="html"> 11008 * <md-grid-list 11009 * md-cols-sm="2" 11010 * md-cols-md="4" 11011 * md-cols-lg="8" 11012 * md-cols-gt-lg="12" 11013 * ...> 11014 * <md-grid-tile></md-grid-tile> 11015 * </md-grid-list> 11016 * </hljs> 11017 */ 11018 function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) { 11019 return { 11020 restrict: 'E', 11021 controller: GridListController, 11022 scope: { 11023 mdOnLayout: '&' 11024 }, 11025 link: postLink 11026 }; 11027 11028 function postLink(scope, element, attrs, ctrl) { 11029 // Apply semantics 11030 element.attr('role', 'list'); 11031 11032 // Provide the controller with a way to trigger layouts. 11033 ctrl.layoutDelegate = layoutDelegate; 11034 11035 var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout), 11036 unwatchAttrs = watchMedia(); 11037 scope.$on('$destroy', unwatchMedia); 11038 11039 /** 11040 * Watches for changes in media, invalidating layout as necessary. 11041 */ 11042 function watchMedia() { 11043 for (var mediaName in $mdConstant.MEDIA) { 11044 $mdMedia(mediaName); // initialize 11045 $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) 11046 .addListener(invalidateLayout); 11047 } 11048 return $mdMedia.watchResponsiveAttributes( 11049 ['md-cols', 'md-row-height', 'md-gutter'], attrs, layoutIfMediaMatch); 11050 } 11051 11052 function unwatchMedia() { 11053 ctrl.layoutDelegate = angular.noop; 11054 11055 unwatchAttrs(); 11056 for (var mediaName in $mdConstant.MEDIA) { 11057 $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) 11058 .removeListener(invalidateLayout); 11059 } 11060 } 11061 11062 /** 11063 * Performs grid layout if the provided mediaName matches the currently 11064 * active media type. 11065 */ 11066 function layoutIfMediaMatch(mediaName) { 11067 if (mediaName == null) { 11068 // TODO(shyndman): It would be nice to only layout if we have 11069 // instances of attributes using this media type 11070 ctrl.invalidateLayout(); 11071 } else if ($mdMedia(mediaName)) { 11072 ctrl.invalidateLayout(); 11073 } 11074 } 11075 11076 var lastLayoutProps; 11077 11078 /** 11079 * Invokes the layout engine, and uses its results to lay out our 11080 * tile elements. 11081 * 11082 * @param {boolean} tilesInvalidated Whether tiles have been 11083 * added/removed/moved since the last layout. This is to avoid situations 11084 * where tiles are replaced with properties identical to their removed 11085 * counterparts. 11086 */ 11087 function layoutDelegate(tilesInvalidated) { 11088 var tiles = getTileElements(); 11089 var props = { 11090 tileSpans: getTileSpans(tiles), 11091 colCount: getColumnCount(), 11092 rowMode: getRowMode(), 11093 rowHeight: getRowHeight(), 11094 gutter: getGutter() 11095 }; 11096 11097 if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) { 11098 return; 11099 } 11100 11101 var performance = 11102 $mdGridLayout(props.colCount, props.tileSpans, tiles) 11103 .map(function(tilePositions, rowCount) { 11104 return { 11105 grid: { 11106 element: element, 11107 style: getGridStyle(props.colCount, rowCount, 11108 props.gutter, props.rowMode, props.rowHeight) 11109 }, 11110 tiles: tilePositions.map(function(ps, i) { 11111 return { 11112 element: angular.element(tiles[i]), 11113 style: getTileStyle(ps.position, ps.spans, 11114 props.colCount, rowCount, 11115 props.gutter, props.rowMode, props.rowHeight) 11116 } 11117 }) 11118 } 11119 }) 11120 .reflow() 11121 .performance(); 11122 11123 // Report layout 11124 scope.mdOnLayout({ 11125 $event: { 11126 performance: performance 11127 } 11128 }); 11129 11130 lastLayoutProps = props; 11131 } 11132 11133 // Use $interpolate to do some simple string interpolation as a convenience. 11134 11135 var startSymbol = $interpolate.startSymbol(); 11136 var endSymbol = $interpolate.endSymbol(); 11137 11138 // Returns an expression wrapped in the interpolator's start and end symbols. 11139 function expr(exprStr) { 11140 return startSymbol + exprStr + endSymbol; 11141 } 11142 11143 // The amount of space a single 1x1 tile would take up (either width or height), used as 11144 // a basis for other calculations. This consists of taking the base size percent (as would be 11145 // if evenly dividing the size between cells), and then subtracting the size of one gutter. 11146 // However, since there are no gutters on the edges, each tile only uses a fration 11147 // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per 11148 // tile, and then breaking up the extra gutter on the edge evenly among the cells). 11149 var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')'); 11150 11151 // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value. 11152 // The position comes the size of a 1x1 tile plus gutter for each previous tile in the 11153 // row/column (offset). 11154 var POSITION = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')'); 11155 11156 // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account. 11157 // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back 11158 // in the space that the gutter would normally have used (which was already accounted for in 11159 // the base unit calculation). 11160 var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')'); 11161 11162 /** 11163 * Gets the styles applied to a tile element described by the given parameters. 11164 * @param {{row: number, col: number}} position The row and column indices of the tile. 11165 * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile. 11166 * @param {number} colCount The number of columns. 11167 * @param {number} rowCount The number of rows. 11168 * @param {string} gutter The amount of space between tiles. This will be something like 11169 * '5px' or '2em'. 11170 * @param {string} rowMode The row height mode. Can be one of: 11171 * 'fixed': all rows have a fixed size, given by rowHeight, 11172 * 'ratio': row height defined as a ratio to width, or 11173 * 'fit': fit to the grid-list element height, divinding evenly among rows. 11174 * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and 11175 * for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75). 11176 * @returns {Object} Map of CSS properties to be applied to the style element. Will define 11177 * values for top, left, width, height, marginTop, and paddingTop. 11178 */ 11179 function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) { 11180 // TODO(shyndman): There are style caching opportunities here. 11181 11182 // Percent of the available horizontal space that one column takes up. 11183 var hShare = (1 / colCount) * 100; 11184 11185 // Fraction of the gutter size that each column takes up. 11186 var hGutterShare = (colCount - 1) / colCount; 11187 11188 // Base horizontal size of a column. 11189 var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter}); 11190 11191 // The width and horizontal position of each tile is always calculated the same way, but the 11192 // height and vertical position depends on the rowMode. 11193 var style = { 11194 left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }), 11195 width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }), 11196 // resets 11197 paddingTop: '', 11198 marginTop: '', 11199 top: '', 11200 height: '' 11201 }; 11202 11203 switch (rowMode) { 11204 case 'fixed': 11205 // In fixed mode, simply use the given rowHeight. 11206 style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter }); 11207 style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter }); 11208 break; 11209 11210 case 'ratio': 11211 // Percent of the available vertical space that one row takes up. Here, rowHeight holds 11212 // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333. 11213 var vShare = hShare / rowHeight; 11214 11215 // Base veritcal size of a row. 11216 var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); 11217 11218 // padidngTop and marginTop are used to maintain the given aspect ratio, as 11219 // a percentage-based value for these properties is applied to the *width* of the 11220 // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties 11221 style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter}); 11222 style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter }); 11223 break; 11224 11225 case 'fit': 11226 // Fraction of the gutter size that each column takes up. 11227 var vGutterShare = (rowCount - 1) / rowCount; 11228 11229 // Percent of the available vertical space that one row takes up. 11230 var vShare = (1 / rowCount) * 100; 11231 11232 // Base vertical size of a row. 11233 var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter}); 11234 11235 style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter}); 11236 style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter}); 11237 break; 11238 } 11239 11240 return style; 11241 } 11242 11243 function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) { 11244 var style = {}; 11245 11246 switch(rowMode) { 11247 case 'fixed': 11248 style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter }); 11249 style.paddingBottom = ''; 11250 break; 11251 11252 case 'ratio': 11253 // rowHeight is width / height 11254 var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount, 11255 hShare = (1 / colCount) * 100, 11256 vShare = hShare * (1 / rowHeight), 11257 vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); 11258 11259 style.height = ''; 11260 style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter}); 11261 break; 11262 11263 case 'fit': 11264 // noop, as the height is user set 11265 break; 11266 } 11267 11268 return style; 11269 } 11270 11271 function getTileElements() { 11272 return [].filter.call(element.children(), function(ele) { 11273 return ele.tagName == 'MD-GRID-TILE' && !ele.$$mdDestroyed; 11274 }); 11275 } 11276 11277 /** 11278 * Gets an array of objects containing the rowspan and colspan for each tile. 11279 * @returns {Array<{row: number, col: number}>} 11280 */ 11281 function getTileSpans(tileElements) { 11282 return [].map.call(tileElements, function(ele) { 11283 var ctrl = angular.element(ele).controller('mdGridTile'); 11284 return { 11285 row: parseInt( 11286 $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1, 11287 col: parseInt( 11288 $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1 11289 }; 11290 }); 11291 } 11292 11293 function getColumnCount() { 11294 var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10); 11295 if (isNaN(colCount)) { 11296 throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value'; 11297 } 11298 return colCount; 11299 } 11300 11301 function getGutter() { 11302 return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1); 11303 } 11304 11305 function getRowHeight() { 11306 var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); 11307 if (!rowHeight) { 11308 throw 'md-grid-list: md-row-height attribute was not found'; 11309 } 11310 11311 switch (getRowMode()) { 11312 case 'fixed': 11313 return applyDefaultUnit(rowHeight); 11314 case 'ratio': 11315 var whRatio = rowHeight.split(':'); 11316 return parseFloat(whRatio[0]) / parseFloat(whRatio[1]); 11317 case 'fit': 11318 return 0; // N/A 11319 } 11320 } 11321 11322 function getRowMode() { 11323 var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); 11324 if (!rowHeight) { 11325 throw 'md-grid-list: md-row-height attribute was not found'; 11326 } 11327 11328 if (rowHeight == 'fit') { 11329 return 'fit'; 11330 } else if (rowHeight.indexOf(':') !== -1) { 11331 return 'ratio'; 11332 } else { 11333 return 'fixed'; 11334 } 11335 } 11336 11337 function applyDefaultUnit(val) { 11338 return /\D$/.test(val) ? val : val + 'px'; 11339 } 11340 } 11341 } 11342 GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"]; 11343 11344 /* @ngInject */ 11345 function GridListController($mdUtil) { 11346 this.layoutInvalidated = false; 11347 this.tilesInvalidated = false; 11348 this.$timeout_ = $mdUtil.nextTick; 11349 this.layoutDelegate = angular.noop; 11350 } 11351 GridListController.$inject = ["$mdUtil"]; 11352 11353 GridListController.prototype = { 11354 invalidateTiles: function() { 11355 this.tilesInvalidated = true; 11356 this.invalidateLayout(); 11357 }, 11358 11359 invalidateLayout: function() { 11360 if (this.layoutInvalidated) { 11361 return; 11362 } 11363 this.layoutInvalidated = true; 11364 this.$timeout_(angular.bind(this, this.layout)); 11365 }, 11366 11367 layout: function() { 11368 try { 11369 this.layoutDelegate(this.tilesInvalidated); 11370 } finally { 11371 this.layoutInvalidated = false; 11372 this.tilesInvalidated = false; 11373 } 11374 } 11375 }; 11376 11377 11378 /* @ngInject */ 11379 function GridLayoutFactory($mdUtil) { 11380 var defaultAnimator = GridTileAnimator; 11381 11382 /** 11383 * Set the reflow animator callback 11384 */ 11385 GridLayout.animateWith = function(customAnimator) { 11386 defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator; 11387 }; 11388 11389 return GridLayout; 11390 11391 /** 11392 * Publish layout function 11393 */ 11394 function GridLayout(colCount, tileSpans) { 11395 var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime; 11396 11397 layoutTime = $mdUtil.time(function() { 11398 layoutInfo = calculateGridFor(colCount, tileSpans); 11399 }); 11400 11401 return self = { 11402 11403 /** 11404 * An array of objects describing each tile's position in the grid. 11405 */ 11406 layoutInfo: function() { 11407 return layoutInfo; 11408 }, 11409 11410 /** 11411 * Maps grid positioning to an element and a set of styles using the 11412 * provided updateFn. 11413 */ 11414 map: function(updateFn) { 11415 mapTime = $mdUtil.time(function() { 11416 var info = self.layoutInfo(); 11417 gridStyles = updateFn(info.positioning, info.rowCount); 11418 }); 11419 return self; 11420 }, 11421 11422 /** 11423 * Default animator simply sets the element.css( <styles> ). An alternate 11424 * animator can be provided as an argument. The function has the following 11425 * signature: 11426 * 11427 * function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>) 11428 */ 11429 reflow: function(animatorFn) { 11430 reflowTime = $mdUtil.time(function() { 11431 var animator = animatorFn || defaultAnimator; 11432 animator(gridStyles.grid, gridStyles.tiles); 11433 }); 11434 return self; 11435 }, 11436 11437 /** 11438 * Timing for the most recent layout run. 11439 */ 11440 performance: function() { 11441 return { 11442 tileCount: tileSpans.length, 11443 layoutTime: layoutTime, 11444 mapTime: mapTime, 11445 reflowTime: reflowTime, 11446 totalTime: layoutTime + mapTime + reflowTime 11447 }; 11448 } 11449 }; 11450 } 11451 11452 /** 11453 * Default Gridlist animator simple sets the css for each element; 11454 * NOTE: any transitions effects must be manually set in the CSS. 11455 * e.g. 11456 * 11457 * md-grid-tile { 11458 * transition: all 700ms ease-out 50ms; 11459 * } 11460 * 11461 */ 11462 function GridTileAnimator(grid, tiles) { 11463 grid.element.css(grid.style); 11464 tiles.forEach(function(t) { 11465 t.element.css(t.style); 11466 }) 11467 } 11468 11469 /** 11470 * Calculates the positions of tiles. 11471 * 11472 * The algorithm works as follows: 11473 * An Array<Number> with length colCount (spaceTracker) keeps track of 11474 * available tiling positions, where elements of value 0 represents an 11475 * empty position. Space for a tile is reserved by finding a sequence of 11476 * 0s with length <= than the tile's colspan. When such a space has been 11477 * found, the occupied tile positions are incremented by the tile's 11478 * rowspan value, as these positions have become unavailable for that 11479 * many rows. 11480 * 11481 * If the end of a row has been reached without finding space for the 11482 * tile, spaceTracker's elements are each decremented by 1 to a minimum 11483 * of 0. Rows are searched in this fashion until space is found. 11484 */ 11485 function calculateGridFor(colCount, tileSpans) { 11486 var curCol = 0, 11487 curRow = 0, 11488 spaceTracker = newSpaceTracker(); 11489 11490 return { 11491 positioning: tileSpans.map(function(spans, i) { 11492 return { 11493 spans: spans, 11494 position: reserveSpace(spans, i) 11495 }; 11496 }), 11497 rowCount: curRow + Math.max.apply(Math, spaceTracker) 11498 }; 11499 11500 function reserveSpace(spans, i) { 11501 if (spans.col > colCount) { 11502 throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' + 11503 '(' + spans.col + ') that exceeds the column count ' + 11504 '(' + colCount + ')'; 11505 } 11506 11507 var start = 0, 11508 end = 0; 11509 11510 // TODO(shyndman): This loop isn't strictly necessary if you can 11511 // determine the minimum number of rows before a space opens up. To do 11512 // this, recognize that you've iterated across an entire row looking for 11513 // space, and if so fast-forward by the minimum rowSpan count. Repeat 11514 // until the required space opens up. 11515 while (end - start < spans.col) { 11516 if (curCol >= colCount) { 11517 nextRow(); 11518 continue; 11519 } 11520 11521 start = spaceTracker.indexOf(0, curCol); 11522 if (start === -1 || (end = findEnd(start + 1)) === -1) { 11523 start = end = 0; 11524 nextRow(); 11525 continue; 11526 } 11527 11528 curCol = end + 1; 11529 } 11530 11531 adjustRow(start, spans.col, spans.row); 11532 curCol = start + spans.col; 11533 11534 return { 11535 col: start, 11536 row: curRow 11537 }; 11538 } 11539 11540 function nextRow() { 11541 curCol = 0; 11542 curRow++; 11543 adjustRow(0, colCount, -1); // Decrement row spans by one 11544 } 11545 11546 function adjustRow(from, cols, by) { 11547 for (var i = from; i < from + cols; i++) { 11548 spaceTracker[i] = Math.max(spaceTracker[i] + by, 0); 11549 } 11550 } 11551 11552 function findEnd(start) { 11553 var i; 11554 for (i = start; i < spaceTracker.length; i++) { 11555 if (spaceTracker[i] !== 0) { 11556 return i; 11557 } 11558 } 11559 11560 if (i === spaceTracker.length) { 11561 return i; 11562 } 11563 } 11564 11565 function newSpaceTracker() { 11566 var tracker = []; 11567 for (var i = 0; i < colCount; i++) { 11568 tracker.push(0); 11569 } 11570 return tracker; 11571 } 11572 } 11573 } 11574 GridLayoutFactory.$inject = ["$mdUtil"]; 11575 11576 /** 11577 * @ngdoc directive 11578 * @name mdGridTile 11579 * @module material.components.gridList 11580 * @restrict E 11581 * @description 11582 * Tiles contain the content of an `md-grid-list`. They span one or more grid 11583 * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to 11584 * display secondary content. 11585 * 11586 * ### Responsive Attributes 11587 * 11588 * The `md-grid-tile` directive supports "responsive" attributes, which allow 11589 * different `md-rowspan` and `md-colspan` values depending on the currently 11590 * matching media query. 11591 * 11592 * In order to set a responsive attribute, first define the fallback value with 11593 * the standard attribute name, then add additional attributes with the 11594 * following convention: `{base-attribute-name}-{media-query-name}="{value}"` 11595 * (ie. `md-colspan-sm="4"`) 11596 * 11597 * @param {number=} md-colspan The number of columns to span (default 1). Cannot 11598 * exceed the number of columns in the grid. Supports interpolation. 11599 * @param {number=} md-rowspan The number of rows to span (default 1). Supports 11600 * interpolation. 11601 * 11602 * @usage 11603 * With header: 11604 * <hljs lang="html"> 11605 * <md-grid-tile> 11606 * <md-grid-tile-header> 11607 * <h3>This is a header</h3> 11608 * </md-grid-tile-header> 11609 * </md-grid-tile> 11610 * </hljs> 11611 * 11612 * With footer: 11613 * <hljs lang="html"> 11614 * <md-grid-tile> 11615 * <md-grid-tile-footer> 11616 * <h3>This is a footer</h3> 11617 * </md-grid-tile-footer> 11618 * </md-grid-tile> 11619 * </hljs> 11620 * 11621 * Spanning multiple rows/columns: 11622 * <hljs lang="html"> 11623 * <md-grid-tile md-colspan="2" md-rowspan="3"> 11624 * </md-grid-tile> 11625 * </hljs> 11626 * 11627 * Responsive attributes: 11628 * <hljs lang="html"> 11629 * <md-grid-tile md-colspan="1" md-colspan-sm="3" md-colspan-md="5"> 11630 * </md-grid-tile> 11631 * </hljs> 11632 */ 11633 function GridTileDirective($mdMedia) { 11634 return { 11635 restrict: 'E', 11636 require: '^mdGridList', 11637 template: '<figure ng-transclude></figure>', 11638 transclude: true, 11639 scope: {}, 11640 // Simple controller that exposes attributes to the grid directive 11641 controller: ["$attrs", function($attrs) { 11642 this.$attrs = $attrs; 11643 }], 11644 link: postLink 11645 }; 11646 11647 function postLink(scope, element, attrs, gridCtrl) { 11648 // Apply semantics 11649 element.attr('role', 'listitem'); 11650 11651 // If our colspan or rowspan changes, trigger a layout 11652 var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'], 11653 attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout)); 11654 11655 // Tile registration/deregistration 11656 gridCtrl.invalidateTiles(); 11657 scope.$on('$destroy', function() { 11658 // Mark the tile as destroyed so it is no longer considered in layout, 11659 // even if the DOM element sticks around (like during a leave animation) 11660 element[0].$$mdDestroyed = true; 11661 unwatchAttrs(); 11662 gridCtrl.invalidateLayout(); 11663 }); 11664 11665 if (angular.isDefined(scope.$parent.$index)) { 11666 scope.$watch(function() { return scope.$parent.$index; }, 11667 function indexChanged(newIdx, oldIdx) { 11668 if (newIdx === oldIdx) { 11669 return; 11670 } 11671 gridCtrl.invalidateTiles(); 11672 }); 11673 } 11674 } 11675 } 11676 GridTileDirective.$inject = ["$mdMedia"]; 11677 11678 11679 function GridTileCaptionDirective() { 11680 return { 11681 template: '<figcaption ng-transclude></figcaption>', 11682 transclude: true 11683 }; 11684 } 11685 11686 })(); 11687 (function(){ 11688 "use strict"; 11689 11690 /** 11691 * @ngdoc module 11692 * @name material.components.icon 11693 * @description 11694 * Icon 11695 */ 11696 angular.module('material.components.icon', ['material.core']); 11697 11698 })(); 11699 (function(){ 11700 "use strict"; 11701 11702 /** 11703 * @ngdoc module 11704 * @name material.components.input 11705 */ 11706 11707 angular.module('material.components.input', [ 11708 'material.core' 11709 ]) 11710 .directive('mdInputContainer', mdInputContainerDirective) 11711 .directive('label', labelDirective) 11712 .directive('input', inputTextareaDirective) 11713 .directive('textarea', inputTextareaDirective) 11714 .directive('mdMaxlength', mdMaxlengthDirective) 11715 .directive('placeholder', placeholderDirective) 11716 .directive('ngMessages', ngMessagesDirective) 11717 .directive('ngMessage', ngMessageDirective) 11718 .directive('ngMessageExp', ngMessageDirective) 11719 .directive('mdSelectOnFocus', mdSelectOnFocusDirective) 11720 11721 .animation('.md-input-invalid', mdInputInvalidMessagesAnimation) 11722 .animation('.md-input-messages-animation', ngMessagesAnimation) 11723 .animation('.md-input-message-animation', ngMessageAnimation); 11724 11725 /** 11726 * @ngdoc directive 11727 * @name mdInputContainer 11728 * @module material.components.input 11729 * 11730 * @restrict E 11731 * 11732 * @description 11733 * `<md-input-container>` is the parent of any input or textarea element. 11734 * 11735 * Input and textarea elements will not behave properly unless the md-input-container 11736 * parent is provided. 11737 * 11738 * A single `<md-input-container>` should contain only one `<input>` element, otherwise it will throw an error. 11739 * 11740 * <b>Exception:</b> Hidden inputs (`<input type="hidden" />`) are ignored and will not throw an error, so 11741 * you may combine these with other inputs. 11742 * 11743 * @param md-is-error {expression=} When the given expression evaluates to true, the input container 11744 * will go into error state. Defaults to erroring if the input has been touched and is invalid. 11745 * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating 11746 * labels. 11747 * 11748 * @usage 11749 * <hljs lang="html"> 11750 * 11751 * <md-input-container> 11752 * <label>Username</label> 11753 * <input type="text" ng-model="user.name"> 11754 * </md-input-container> 11755 * 11756 * <md-input-container> 11757 * <label>Description</label> 11758 * <textarea ng-model="user.description"></textarea> 11759 * </md-input-container> 11760 * 11761 * </hljs> 11762 * 11763 * <h3>When disabling floating labels</h3> 11764 * <hljs lang="html"> 11765 * 11766 * <md-input-container md-no-float> 11767 * <input type="text" placeholder="Non-Floating Label"> 11768 * </md-input-container> 11769 * 11770 * </hljs> 11771 */ 11772 function mdInputContainerDirective($mdTheming, $parse) { 11773 ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"]; 11774 return { 11775 restrict: 'E', 11776 link: postLink, 11777 controller: ContainerCtrl 11778 }; 11779 11780 function postLink(scope, element, attr) { 11781 $mdTheming(element); 11782 if (element.find('md-icon').length) element.addClass('md-has-icon'); 11783 } 11784 11785 function ContainerCtrl($scope, $element, $attrs, $animate) { 11786 var self = this; 11787 11788 self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError); 11789 11790 self.delegateClick = function() { 11791 self.input.focus(); 11792 }; 11793 self.element = $element; 11794 self.setFocused = function(isFocused) { 11795 $element.toggleClass('md-input-focused', !!isFocused); 11796 }; 11797 self.setHasValue = function(hasValue) { 11798 $element.toggleClass('md-input-has-value', !!hasValue); 11799 }; 11800 self.setHasPlaceholder = function(hasPlaceholder) { 11801 $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder); 11802 }; 11803 self.setInvalid = function(isInvalid) { 11804 if (isInvalid) { 11805 $animate.addClass($element, 'md-input-invalid'); 11806 } else { 11807 $animate.removeClass($element, 'md-input-invalid'); 11808 } 11809 }; 11810 $scope.$watch(function() { 11811 return self.label && self.input; 11812 }, function(hasLabelAndInput) { 11813 if (hasLabelAndInput && !self.label.attr('for')) { 11814 self.label.attr('for', self.input.attr('id')); 11815 } 11816 }); 11817 } 11818 } 11819 mdInputContainerDirective.$inject = ["$mdTheming", "$parse"]; 11820 11821 function labelDirective() { 11822 return { 11823 restrict: 'E', 11824 require: '^?mdInputContainer', 11825 link: function(scope, element, attr, containerCtrl) { 11826 if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return; 11827 11828 containerCtrl.label = element; 11829 scope.$on('$destroy', function() { 11830 containerCtrl.label = null; 11831 }); 11832 } 11833 }; 11834 } 11835 11836 /** 11837 * @ngdoc directive 11838 * @name mdInput 11839 * @restrict E 11840 * @module material.components.input 11841 * 11842 * @description 11843 * You can use any `<input>` or `<textarea>` element as a child of an `<md-input-container>`. This 11844 * allows you to build complex forms for data entry. 11845 * 11846 * @param {number=} md-maxlength The maximum number of characters allowed in this input. If this is 11847 * specified, a character counter will be shown underneath the input.<br/><br/> 11848 * The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't 11849 * want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength` 11850 * or maxlength attributes. 11851 * @param {string=} aria-label Aria-label is required when no label is present. A warning message 11852 * will be logged in the console if not present. 11853 * @param {string=} placeholder An alternative approach to using aria-label when the label is not 11854 * PRESENT. The placeholder text is copied to the aria-label attribute. 11855 * @param md-no-autogrow {boolean=} When present, textareas will not grow automatically. 11856 * @param md-detect-hidden {boolean=} When present, textareas will be sized properly when they are 11857 * revealed after being hidden. This is off by default for performance reasons because it 11858 * guarantees a reflow every digest cycle. 11859 * 11860 * @usage 11861 * <hljs lang="html"> 11862 * <md-input-container> 11863 * <label>Color</label> 11864 * <input type="text" ng-model="color" required md-maxlength="10"> 11865 * </md-input-container> 11866 * </hljs> 11867 * 11868 * <h3>With Errors</h3> 11869 * 11870 * `md-input-container` also supports errors using the standard `ng-messages` directives and 11871 * animates the messages when they become visible using from the `ngEnter`/`ngLeave` events or 11872 * the `ngShow`/`ngHide` events. 11873 * 11874 * By default, the messages will be hidden until the input is in an error state. This is based off 11875 * of the `md-is-error` expression of the `md-input-container`. This gives the user a chance to 11876 * fill out the form before the errors become visible. 11877 * 11878 * <hljs lang="html"> 11879 * <form name="colorForm"> 11880 * <md-input-container> 11881 * <label>Favorite Color</label> 11882 * <input name="favoriteColor" ng-model="favoriteColor" required> 11883 * <div ng-messages="userForm.lastName.$error"> 11884 * <div ng-message="required">This is required!</div> 11885 * </div> 11886 * </md-input-container> 11887 * </form> 11888 * </hljs> 11889 * 11890 * We automatically disable this auto-hiding functionality if you provide any of the following 11891 * visibility directives on the `ng-messages` container: 11892 * 11893 * - `ng-if` 11894 * - `ng-show`/`ng-hide` 11895 * - `ng-switch-when`/`ng-switch-default` 11896 * 11897 * You can also disable this functionality manually by adding the `md-auto-hide="false"` expression 11898 * to the `ng-messages` container. This may be helpful if you always want to see the error messages 11899 * or if you are building your own visibilty directive. 11900 * 11901 * _<b>Note:</b> The `md-auto-hide` attribute is a static string that is only checked upon 11902 * initialization of the `ng-messages` directive to see if it equals the string `false`._ 11903 * 11904 * <hljs lang="html"> 11905 * <form name="userForm"> 11906 * <md-input-container> 11907 * <label>Last Name</label> 11908 * <input name="lastName" ng-model="lastName" required md-maxlength="10" minlength="4"> 11909 * <div ng-messages="userForm.lastName.$error" ng-show="userForm.lastName.$dirty"> 11910 * <div ng-message="required">This is required!</div> 11911 * <div ng-message="md-maxlength">That's too long!</div> 11912 * <div ng-message="minlength">That's too short!</div> 11913 * </div> 11914 * </md-input-container> 11915 * <md-input-container> 11916 * <label>Biography</label> 11917 * <textarea name="bio" ng-model="biography" required md-maxlength="150"></textarea> 11918 * <div ng-messages="userForm.bio.$error" ng-show="userForm.bio.$dirty"> 11919 * <div ng-message="required">This is required!</div> 11920 * <div ng-message="md-maxlength">That's too long!</div> 11921 * </div> 11922 * </md-input-container> 11923 * <md-input-container> 11924 * <input aria-label='title' ng-model='title'> 11925 * </md-input-container> 11926 * <md-input-container> 11927 * <input placeholder='title' ng-model='title'> 11928 * </md-input-container> 11929 * </form> 11930 * </hljs> 11931 * 11932 * <h3>Notes</h3> 11933 * 11934 * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). 11935 * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). 11936 * 11937 * The `md-input` and `md-input-container` directives use very specific positioning to achieve the 11938 * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the 11939 * `<md-input-container>` tags. Instead, use relative or absolute positioning. 11940 * 11941 */ 11942 11943 function inputTextareaDirective($mdUtil, $window, $mdAria) { 11944 return { 11945 restrict: 'E', 11946 require: ['^?mdInputContainer', '?ngModel'], 11947 link: postLink 11948 }; 11949 11950 function postLink(scope, element, attr, ctrls) { 11951 11952 var containerCtrl = ctrls[0]; 11953 var hasNgModel = !!ctrls[1]; 11954 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); 11955 var isReadonly = angular.isDefined(attr.readonly); 11956 11957 if (!containerCtrl) return; 11958 if (attr.type === 'hidden') { 11959 element.attr('aria-hidden', 'true'); 11960 return; 11961 } else if (containerCtrl.input) { 11962 throw new Error("<md-input-container> can only have *one* <input>, <textarea> or <md-select> child element!"); 11963 } 11964 containerCtrl.input = element; 11965 11966 // Add an error spacer div after our input to provide space for the char counter and any ng-messages 11967 var errorsSpacer = angular.element('<div class="md-errors-spacer">'); 11968 element.after(errorsSpacer); 11969 11970 if (!containerCtrl.label) { 11971 $mdAria.expect(element, 'aria-label', element.attr('placeholder')); 11972 } 11973 11974 element.addClass('md-input'); 11975 if (!element.attr('id')) { 11976 element.attr('id', 'input_' + $mdUtil.nextUid()); 11977 } 11978 11979 if (element[0].tagName.toLowerCase() === 'textarea') { 11980 setupTextarea(); 11981 } 11982 11983 // If the input doesn't have an ngModel, it may have a static value. For that case, 11984 // we have to do one initial check to determine if the container should be in the 11985 // "has a value" state. 11986 if (!hasNgModel) { 11987 inputCheckValue(); 11988 } 11989 11990 var isErrorGetter = containerCtrl.isErrorGetter || function() { 11991 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || isParentFormSubmitted()); 11992 }; 11993 11994 var isParentFormSubmitted = function () { 11995 var parent = $mdUtil.getClosest(element, 'form'); 11996 var form = parent ? angular.element(parent).controller('form') : null; 11997 11998 return form ? form.$submitted : false; 11999 }; 12000 12001 scope.$watch(isErrorGetter, containerCtrl.setInvalid); 12002 12003 ngModelCtrl.$parsers.push(ngModelPipelineCheckValue); 12004 ngModelCtrl.$formatters.push(ngModelPipelineCheckValue); 12005 12006 element.on('input', inputCheckValue); 12007 12008 if (!isReadonly) { 12009 element 12010 .on('focus', function(ev) { 12011 $mdUtil.nextTick(function() { 12012 containerCtrl.setFocused(true); 12013 }); 12014 }) 12015 .on('blur', function(ev) { 12016 $mdUtil.nextTick(function() { 12017 containerCtrl.setFocused(false); 12018 inputCheckValue(); 12019 }); 12020 }); 12021 } 12022 12023 //ngModelCtrl.$setTouched(); 12024 //if( ngModelCtrl.$invalid ) containerCtrl.setInvalid(); 12025 12026 scope.$on('$destroy', function() { 12027 containerCtrl.setFocused(false); 12028 containerCtrl.setHasValue(false); 12029 containerCtrl.input = null; 12030 }); 12031 12032 /** 12033 * 12034 */ 12035 function ngModelPipelineCheckValue(arg) { 12036 containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg)); 12037 return arg; 12038 } 12039 12040 function inputCheckValue() { 12041 // An input's value counts if its length > 0, 12042 // or if the input's validity state says it has bad input (eg string in a number input) 12043 containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity || {}).badInput); 12044 } 12045 12046 function setupTextarea() { 12047 if (angular.isDefined(element.attr('md-no-autogrow'))) { 12048 return; 12049 } 12050 12051 var node = element[0]; 12052 var container = containerCtrl.element[0]; 12053 12054 var min_rows = NaN; 12055 var lineHeight = null; 12056 // can't check if height was or not explicitly set, 12057 // so rows attribute will take precedence if present 12058 if (node.hasAttribute('rows')) { 12059 min_rows = parseInt(node.getAttribute('rows')); 12060 } 12061 12062 var onChangeTextarea = $mdUtil.debounce(growTextarea, 1); 12063 12064 function pipelineListener(value) { 12065 onChangeTextarea(); 12066 return value; 12067 } 12068 12069 if (ngModelCtrl) { 12070 ngModelCtrl.$formatters.push(pipelineListener); 12071 ngModelCtrl.$viewChangeListeners.push(pipelineListener); 12072 } else { 12073 onChangeTextarea(); 12074 } 12075 element.on('keydown input', onChangeTextarea); 12076 12077 if (isNaN(min_rows)) { 12078 element.attr('rows', '1'); 12079 12080 element.on('scroll', onScroll); 12081 } 12082 12083 angular.element($window).on('resize', onChangeTextarea); 12084 12085 scope.$on('$destroy', function() { 12086 angular.element($window).off('resize', onChangeTextarea); 12087 }); 12088 12089 function growTextarea() { 12090 // sets the md-input-container height to avoid jumping around 12091 container.style.height = container.offsetHeight + 'px'; 12092 12093 // temporarily disables element's flex so its height 'runs free' 12094 element.addClass('md-no-flex'); 12095 12096 if (isNaN(min_rows)) { 12097 node.style.height = "auto"; 12098 node.scrollTop = 0; 12099 var height = getHeight(); 12100 if (height) node.style.height = height + 'px'; 12101 } else { 12102 node.setAttribute("rows", 1); 12103 12104 if (!lineHeight) { 12105 node.style.minHeight = '0'; 12106 12107 lineHeight = element.prop('clientHeight'); 12108 12109 node.style.minHeight = null; 12110 } 12111 12112 var rows = Math.min(min_rows, Math.round(node.scrollHeight / lineHeight)); 12113 node.setAttribute("rows", rows); 12114 node.style.height = lineHeight * rows + "px"; 12115 } 12116 12117 // reset everything back to normal 12118 element.removeClass('md-no-flex'); 12119 container.style.height = 'auto'; 12120 } 12121 12122 function getHeight() { 12123 var line = node.scrollHeight - node.offsetHeight; 12124 return node.offsetHeight + (line > 0 ? line : 0); 12125 } 12126 12127 function onScroll(e) { 12128 node.scrollTop = 0; 12129 // for smooth new line adding 12130 var line = node.scrollHeight - node.offsetHeight; 12131 var height = node.offsetHeight + line; 12132 node.style.height = height + 'px'; 12133 } 12134 12135 // Attach a watcher to detect when the textarea gets shown. 12136 if (angular.isDefined(element.attr('md-detect-hidden'))) { 12137 12138 var handleHiddenChange = function() { 12139 var wasHidden = false; 12140 12141 return function() { 12142 var isHidden = node.offsetHeight === 0; 12143 12144 if (isHidden === false && wasHidden === true) { 12145 growTextarea(); 12146 } 12147 12148 wasHidden = isHidden; 12149 }; 12150 }(); 12151 12152 // Check every digest cycle whether the visibility of the textarea has changed. 12153 // Queue up to run after the digest cycle is complete. 12154 scope.$watch(function() { 12155 $mdUtil.nextTick(handleHiddenChange, false); 12156 return true; 12157 }); 12158 } 12159 } 12160 } 12161 } 12162 inputTextareaDirective.$inject = ["$mdUtil", "$window", "$mdAria"]; 12163 12164 function mdMaxlengthDirective($animate, $mdUtil) { 12165 return { 12166 restrict: 'A', 12167 require: ['ngModel', '^mdInputContainer'], 12168 link: postLink 12169 }; 12170 12171 function postLink(scope, element, attr, ctrls) { 12172 var maxlength; 12173 var ngModelCtrl = ctrls[0]; 12174 var containerCtrl = ctrls[1]; 12175 var charCountEl, errorsSpacer; 12176 12177 // Wait until the next tick to ensure that the input has setup the errors spacer where we will 12178 // append our counter 12179 $mdUtil.nextTick(function() { 12180 errorsSpacer = angular.element(containerCtrl.element[0].querySelector('.md-errors-spacer')); 12181 charCountEl = angular.element('<div class="md-char-counter">'); 12182 12183 // Append our character counter inside the errors spacer 12184 errorsSpacer.append(charCountEl); 12185 12186 // Stop model from trimming. This makes it so whitespace 12187 // over the maxlength still counts as invalid. 12188 attr.$set('ngTrim', 'false'); 12189 12190 ngModelCtrl.$formatters.push(renderCharCount); 12191 ngModelCtrl.$viewChangeListeners.push(renderCharCount); 12192 element.on('input keydown keyup', function() { 12193 renderCharCount(); //make sure it's called with no args 12194 }); 12195 12196 scope.$watch(attr.mdMaxlength, function(value) { 12197 maxlength = value; 12198 if (angular.isNumber(value) && value > 0) { 12199 if (!charCountEl.parent().length) { 12200 $animate.enter(charCountEl, errorsSpacer); 12201 } 12202 renderCharCount(); 12203 } else { 12204 $animate.leave(charCountEl); 12205 } 12206 }); 12207 12208 ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) { 12209 if (!angular.isNumber(maxlength) || maxlength < 0) { 12210 return true; 12211 } 12212 return ( modelValue || element.val() || viewValue || '' ).length <= maxlength; 12213 }; 12214 }); 12215 12216 function renderCharCount(value) { 12217 // If we have not been appended to the body yet; do not render 12218 if (!charCountEl.parent) { 12219 return value; 12220 } 12221 12222 // Force the value into a string since it may be a number, 12223 // which does not have a length property. 12224 charCountEl.text(String(element.val() || value || '').length + '/' + maxlength); 12225 return value; 12226 } 12227 } 12228 } 12229 mdMaxlengthDirective.$inject = ["$animate", "$mdUtil"]; 12230 12231 function placeholderDirective($log) { 12232 return { 12233 restrict: 'A', 12234 require: '^^?mdInputContainer', 12235 priority: 200, 12236 link: postLink 12237 }; 12238 12239 function postLink(scope, element, attr, inputContainer) { 12240 // If there is no input container, just return 12241 if (!inputContainer) return; 12242 12243 var label = inputContainer.element.find('label'); 12244 var hasNoFloat = angular.isDefined(inputContainer.element.attr('md-no-float')); 12245 12246 // If we have a label, or they specify the md-no-float attribute, just return 12247 if ((label && label.length) || hasNoFloat) { 12248 // Add a placeholder class so we can target it in the CSS 12249 inputContainer.setHasPlaceholder(true); 12250 return; 12251 } 12252 12253 // Otherwise, grab/remove the placeholder 12254 var placeholderText = attr.placeholder; 12255 element.removeAttr('placeholder'); 12256 12257 // And add the placeholder text as a separate label 12258 if (inputContainer.input && inputContainer.input[0].nodeName != 'MD-SELECT') { 12259 var placeholder = '<label ng-click="delegateClick()">' + placeholderText + '</label>'; 12260 12261 inputContainer.element.addClass('md-icon-float'); 12262 inputContainer.element.prepend(placeholder); 12263 } 12264 } 12265 } 12266 placeholderDirective.$inject = ["$log"]; 12267 12268 /** 12269 * @ngdoc directive 12270 * @name mdSelectOnFocus 12271 * @module material.components.input 12272 * 12273 * @restrict A 12274 * 12275 * @description 12276 * The `md-select-on-focus` directive allows you to automatically select the element's input text on focus. 12277 * 12278 * <h3>Notes</h3> 12279 * - The use of `md-select-on-focus` is restricted to `<input>` and `<textarea>` elements. 12280 * 12281 * @usage 12282 * <h3>Using with an Input</h3> 12283 * <hljs lang="html"> 12284 * 12285 * <md-input-container> 12286 * <label>Auto Select</label> 12287 * <input type="text" md-select-on-focus> 12288 * </md-input-container> 12289 * </hljs> 12290 * 12291 * <h3>Using with a Textarea</h3> 12292 * <hljs lang="html"> 12293 * 12294 * <md-input-container> 12295 * <label>Auto Select</label> 12296 * <textarea md-select-on-focus>This text will be selected on focus.</textarea> 12297 * </md-input-container> 12298 * 12299 * </hljs> 12300 */ 12301 function mdSelectOnFocusDirective() { 12302 12303 return { 12304 restrict: 'A', 12305 link: postLink 12306 }; 12307 12308 function postLink(scope, element, attr) { 12309 if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return; 12310 12311 element.on('focus', onFocus); 12312 12313 scope.$on('$destroy', function() { 12314 element.off('focus', onFocus); 12315 }); 12316 12317 function onFocus() { 12318 // Use HTMLInputElement#select to fix firefox select issues 12319 element[0].select(); 12320 } 12321 } 12322 } 12323 12324 var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault']; 12325 function ngMessagesDirective() { 12326 return { 12327 restrict: 'EA', 12328 link: postLink, 12329 12330 // This is optional because we don't want target *all* ngMessage instances, just those inside of 12331 // mdInputContainer. 12332 require: '^^?mdInputContainer' 12333 }; 12334 12335 function postLink(scope, element, attrs, inputContainer) { 12336 // If we are not a child of an input container, don't do anything 12337 if (!inputContainer) return; 12338 12339 // Add our animation class 12340 element.toggleClass('md-input-messages-animation', true); 12341 12342 // Add our md-auto-hide class to automatically hide/show messages when container is invalid 12343 element.toggleClass('md-auto-hide', true); 12344 12345 // If we see some known visibility directives, remove the md-auto-hide class 12346 if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) { 12347 element.toggleClass('md-auto-hide', false); 12348 } 12349 } 12350 12351 function hasVisibiltyDirective(attrs) { 12352 return visibilityDirectives.some(function(attr) { 12353 return attrs[attr]; 12354 }); 12355 } 12356 } 12357 12358 function ngMessageDirective($mdUtil) { 12359 return { 12360 restrict: 'EA', 12361 compile: compile, 12362 priority: 100 12363 }; 12364 12365 function compile(element) { 12366 var inputContainer = $mdUtil.getClosest(element, "md-input-container"); 12367 12368 // If we are not a child of an input container, don't do anything 12369 if (!inputContainer) return; 12370 12371 // Add our animation class 12372 element.toggleClass('md-input-message-animation', true); 12373 12374 return {}; 12375 } 12376 } 12377 ngMessageDirective.$inject = ["$mdUtil"]; 12378 12379 function mdInputInvalidMessagesAnimation($q, $animateCss) { 12380 return { 12381 addClass: function(element, className, done) { 12382 var messages = getMessagesElement(element); 12383 12384 if (className == "md-input-invalid" && messages.hasClass('md-auto-hide')) { 12385 showInputMessages(element, $animateCss, $q).finally(done); 12386 } else { 12387 done(); 12388 } 12389 } 12390 12391 // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire 12392 } 12393 } 12394 mdInputInvalidMessagesAnimation.$inject = ["$q", "$animateCss"]; 12395 12396 function ngMessagesAnimation($q, $animateCss) { 12397 return { 12398 enter: function(element, done) { 12399 showInputMessages(element, $animateCss, $q).finally(done); 12400 }, 12401 12402 leave: function(element, done) { 12403 hideInputMessages(element, $animateCss, $q).finally(done); 12404 }, 12405 12406 addClass: function(element, className, done) { 12407 if (className == "ng-hide") { 12408 hideInputMessages(element, $animateCss, $q).finally(done); 12409 } else { 12410 done(); 12411 } 12412 }, 12413 12414 removeClass: function(element, className, done) { 12415 if (className == "ng-hide") { 12416 showInputMessages(element, $animateCss, $q).finally(done); 12417 } else { 12418 done(); 12419 } 12420 } 12421 } 12422 } 12423 ngMessagesAnimation.$inject = ["$q", "$animateCss"]; 12424 12425 function ngMessageAnimation($animateCss) { 12426 return { 12427 enter: function(element, done) { 12428 var messages = getMessagesElement(element); 12429 12430 // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip 12431 if (messages.hasClass('md-auto-hide')) { 12432 done(); 12433 return; 12434 } 12435 12436 return showMessage(element, $animateCss); 12437 }, 12438 12439 leave: function(element, done) { 12440 return hideMessage(element, $animateCss); 12441 } 12442 } 12443 } 12444 ngMessageAnimation.$inject = ["$animateCss"]; 12445 12446 function showInputMessages(element, $animateCss, $q) { 12447 var animators = [], animator; 12448 var messages = getMessagesElement(element); 12449 12450 angular.forEach(messages.children(), function(child) { 12451 animator = showMessage(angular.element(child), $animateCss); 12452 12453 animators.push(animator.start()); 12454 }); 12455 12456 return $q.all(animators); 12457 } 12458 12459 function hideInputMessages(element, $animateCss, $q) { 12460 var animators = [], animator; 12461 var messages = getMessagesElement(element); 12462 12463 angular.forEach(messages.children(), function(child) { 12464 animator = hideMessage(angular.element(child), $animateCss); 12465 12466 animators.push(animator.start()); 12467 }); 12468 12469 return $q.all(animators); 12470 } 12471 12472 function showMessage(element, $animateCss) { 12473 var height = element[0].offsetHeight; 12474 12475 return $animateCss(element, { 12476 event: 'enter', 12477 structural: true, 12478 from: {"opacity": 0, "margin-top": -height + "px"}, 12479 to: {"opacity": 1, "margin-top": "0"}, 12480 duration: 0.3 12481 }); 12482 } 12483 12484 function hideMessage(element, $animateCss) { 12485 var height = element[0].offsetHeight; 12486 var styles = window.getComputedStyle(element[0]); 12487 12488 // If we are already hidden, just return an empty animation 12489 if (styles.opacity == 0) { 12490 return $animateCss(element, {}); 12491 } 12492 12493 // Otherwise, animate 12494 return $animateCss(element, { 12495 event: 'leave', 12496 structural: true, 12497 from: {"opacity": 1, "margin-top": 0}, 12498 to: {"opacity": 0, "margin-top": -height + "px"}, 12499 duration: 0.3 12500 }); 12501 } 12502 12503 function getInputElement(element) { 12504 var inputContainer = element.controller('mdInputContainer'); 12505 12506 return inputContainer.element; 12507 } 12508 12509 function getMessagesElement(element) { 12510 var input = getInputElement(element); 12511 var selector = 'ng-messages,data-ng-messages,x-ng-messages,' + 12512 '[ng-messages],[data-ng-messages],[x-ng-messages]'; 12513 12514 return angular.element(input[0].querySelector(selector)); 12515 } 12516 12517 })(); 12518 (function(){ 12519 "use strict"; 12520 12521 /** 12522 * @ngdoc module 12523 * @name material.components.menu 12524 */ 12525 12526 angular.module('material.components.menu', [ 12527 'material.core', 12528 'material.components.backdrop' 12529 ]); 12530 12531 })(); 12532 (function(){ 12533 "use strict"; 12534 12535 /** 12536 * @ngdoc module 12537 * @name material.components.list 12538 * @description 12539 * List module 12540 */ 12541 angular.module('material.components.list', [ 12542 'material.core' 12543 ]) 12544 .controller('MdListController', MdListController) 12545 .directive('mdList', mdListDirective) 12546 .directive('mdListItem', mdListItemDirective); 12547 12548 /** 12549 * @ngdoc directive 12550 * @name mdList 12551 * @module material.components.list 12552 * 12553 * @restrict E 12554 * 12555 * @description 12556 * The `<md-list>` directive is a list container for 1..n `<md-list-item>` tags. 12557 * 12558 * @usage 12559 * <hljs lang="html"> 12560 * <md-list> 12561 * <md-list-item class="md-2-line" ng-repeat="item in todos"> 12562 * <md-checkbox ng-model="item.done"></md-checkbox> 12563 * <div class="md-list-item-text"> 12564 * <h3>{{item.title}}</h3> 12565 * <p>{{item.description}}</p> 12566 * </div> 12567 * </md-list-item> 12568 * </md-list> 12569 * </hljs> 12570 */ 12571 12572 function mdListDirective($mdTheming) { 12573 return { 12574 restrict: 'E', 12575 compile: function(tEl) { 12576 tEl[0].setAttribute('role', 'list'); 12577 return $mdTheming; 12578 } 12579 }; 12580 } 12581 mdListDirective.$inject = ["$mdTheming"]; 12582 /** 12583 * @ngdoc directive 12584 * @name mdListItem 12585 * @module material.components.list 12586 * 12587 * @restrict E 12588 * 12589 * @description 12590 * The `<md-list-item>` directive is a container intended for row items in a `<md-list>` container. 12591 * The `md-2-line` and `md-3-line` classes can be added to a `<md-list-item>` 12592 * to increase the height with 22px and 40px respectively. 12593 * 12594 * ## CSS 12595 * `.md-avatar` - class for image avatars 12596 * 12597 * `.md-avatar-icon` - class for icon avatars 12598 * 12599 * `.md-offset` - on content without an avatar 12600 * 12601 * @usage 12602 * <hljs lang="html"> 12603 * <md-list> 12604 * <md-list-item> 12605 * <img class="md-avatar" ng-src="path/to/img"/> 12606 * <span>Item content in list</span> 12607 * </md-list-item> 12608 * <md-list-item> 12609 * <md-icon class="md-avatar-icon" md-svg-icon="communication:phone"></md-icon> 12610 * <span>Item content in list</span> 12611 * </md-list-item> 12612 * </md-list> 12613 * </hljs> 12614 * 12615 * _**Note:** We automatically apply special styling when the inner contents are wrapped inside 12616 * of a `<md-button>` tag. This styling is automatically ignored for `class="md-secondary"` buttons 12617 * and you can include a class of `class="md-exclude"` if you need to use a non-secondary button 12618 * that is inside the list, but does not wrap the contents._ 12619 */ 12620 function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) { 12621 var proxiedTypes = ['md-checkbox', 'md-switch']; 12622 return { 12623 restrict: 'E', 12624 controller: 'MdListController', 12625 compile: function(tEl, tAttrs) { 12626 // Check for proxy controls (no ng-click on parent, and a control inside) 12627 var secondaryItem = tEl[0].querySelector('.md-secondary'); 12628 var hasProxiedElement; 12629 var proxyElement; 12630 12631 tEl[0].setAttribute('role', 'listitem'); 12632 12633 if (tAttrs.ngClick || tAttrs.ngHref || tAttrs.href || tAttrs.uiSref || tAttrs.ngAttrUiSref) { 12634 wrapIn('button'); 12635 } else { 12636 for (var i = 0, type; type = proxiedTypes[i]; ++i) { 12637 if (proxyElement = tEl[0].querySelector(type)) { 12638 hasProxiedElement = true; 12639 break; 12640 } 12641 } 12642 if (hasProxiedElement) { 12643 wrapIn('div'); 12644 } else if (!tEl[0].querySelector('md-button:not(.md-secondary):not(.md-exclude)')) { 12645 tEl.addClass('md-no-proxy'); 12646 } 12647 } 12648 wrapSecondary(); 12649 setupToggleAria(); 12650 12651 12652 function setupToggleAria() { 12653 var toggleTypes = ['md-switch', 'md-checkbox']; 12654 var toggle; 12655 12656 for (var i = 0, toggleType; toggleType = toggleTypes[i]; ++i) { 12657 if (toggle = tEl.find(toggleType)[0]) { 12658 if (!toggle.hasAttribute('aria-label')) { 12659 var p = tEl.find('p')[0]; 12660 if (!p) return; 12661 toggle.setAttribute('aria-label', 'Toggle ' + p.textContent); 12662 } 12663 } 12664 } 12665 } 12666 12667 function wrapIn(type) { 12668 var container; 12669 if (type == 'div') { 12670 container = angular.element('<div class="md-no-style md-list-item-inner">'); 12671 container.append(tEl.contents()); 12672 tEl.addClass('md-proxy-focus'); 12673 } else { 12674 container = angular.element('<md-button class="md-no-style"><div class="md-list-item-inner"></div></md-button>'); 12675 copyAttributes(tEl[0], container[0]); 12676 container.children().eq(0).append(tEl.contents()); 12677 } 12678 12679 tEl[0].setAttribute('tabindex', '-1'); 12680 tEl.append(container); 12681 } 12682 12683 function wrapSecondary() { 12684 if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) { 12685 $mdAria.expect(secondaryItem, 'aria-label'); 12686 var buttonWrapper = angular.element('<md-button class="md-secondary-container md-icon-button">'); 12687 copyAttributes(secondaryItem, buttonWrapper[0]); 12688 secondaryItem.setAttribute('tabindex', '-1'); 12689 secondaryItem.classList.remove('md-secondary'); 12690 buttonWrapper.append(secondaryItem); 12691 secondaryItem = buttonWrapper[0]; 12692 } 12693 12694 // Check for a secondary item and move it outside 12695 if ( secondaryItem && ( 12696 secondaryItem.hasAttribute('ng-click') || 12697 ( tAttrs.ngClick && 12698 isProxiedElement(secondaryItem) ) 12699 )) { 12700 tEl.addClass('md-with-secondary'); 12701 tEl.append(secondaryItem); 12702 } 12703 } 12704 12705 function copyAttributes(item, wrapper) { 12706 var copiedAttrs = ['ng-if', 'ng-click', 'aria-label', 'ng-disabled', 12707 'ui-sref', 'href', 'ng-href', 'ng-attr-ui-sref']; 12708 angular.forEach(copiedAttrs, function(attr) { 12709 if (item.hasAttribute(attr)) { 12710 wrapper.setAttribute(attr, item.getAttribute(attr)); 12711 item.removeAttribute(attr); 12712 } 12713 }); 12714 } 12715 12716 function isProxiedElement(el) { 12717 return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1; 12718 } 12719 12720 function isButton(el) { 12721 var nodeName = el.nodeName.toUpperCase(); 12722 12723 return nodeName == "MD-BUTTON" || nodeName == "BUTTON"; 12724 } 12725 12726 return postLink; 12727 12728 function postLink($scope, $element, $attr, ctrl) { 12729 12730 var proxies = [], 12731 firstChild = $element[0].firstElementChild, 12732 hasClick = firstChild && hasClickEvent(firstChild); 12733 12734 computeProxies(); 12735 computeClickable(); 12736 12737 if ($element.hasClass('md-proxy-focus') && proxies.length) { 12738 angular.forEach(proxies, function(proxy) { 12739 proxy = angular.element(proxy); 12740 12741 $scope.mouseActive = false; 12742 proxy.on('mousedown', function() { 12743 $scope.mouseActive = true; 12744 $timeout(function(){ 12745 $scope.mouseActive = false; 12746 }, 100); 12747 }) 12748 .on('focus', function() { 12749 if ($scope.mouseActive === false) { $element.addClass('md-focused'); } 12750 proxy.on('blur', function proxyOnBlur() { 12751 $element.removeClass('md-focused'); 12752 proxy.off('blur', proxyOnBlur); 12753 }); 12754 }); 12755 }); 12756 } 12757 12758 function hasClickEvent (element) { 12759 var attr = element.attributes; 12760 for (var i = 0; i < attr.length; i++) { 12761 if ($attr.$normalize(attr[i].name) === 'ngClick') return true; 12762 } 12763 return false; 12764 } 12765 12766 function computeProxies() { 12767 var children = $element.children(); 12768 if (children.length && !children[0].hasAttribute('ng-click')) { 12769 angular.forEach(proxiedTypes, function(type) { 12770 angular.forEach(firstChild.querySelectorAll(type), function(child) { 12771 proxies.push(child); 12772 }); 12773 }); 12774 } 12775 } 12776 function computeClickable() { 12777 if (proxies.length == 1 || hasClick) { 12778 $element.addClass('md-clickable'); 12779 12780 if (!hasClick) { 12781 ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style'))); 12782 } 12783 } 12784 } 12785 12786 var firstChildKeypressListener = function(e) { 12787 if (e.target.nodeName != 'INPUT' && e.target.nodeName != 'TEXTAREA' && !e.target.isContentEditable) { 12788 var keyCode = e.which || e.keyCode; 12789 if (keyCode == $mdConstant.KEY_CODE.SPACE) { 12790 if (firstChild) { 12791 firstChild.click(); 12792 e.preventDefault(); 12793 e.stopPropagation(); 12794 } 12795 } 12796 } 12797 }; 12798 12799 if (!hasClick && !proxies.length) { 12800 firstChild && firstChild.addEventListener('keypress', firstChildKeypressListener); 12801 } 12802 12803 $element.off('click'); 12804 $element.off('keypress'); 12805 12806 if (proxies.length == 1 && firstChild) { 12807 $element.children().eq(0).on('click', function(e) { 12808 var parentButton = $mdUtil.getClosest(e.target, 'BUTTON'); 12809 if (!parentButton && firstChild.contains(e.target)) { 12810 angular.forEach(proxies, function(proxy) { 12811 if (e.target !== proxy && !proxy.contains(e.target)) { 12812 angular.element(proxy).triggerHandler('click'); 12813 } 12814 }); 12815 } 12816 }); 12817 } 12818 12819 $scope.$on('$destroy', function () { 12820 firstChild && firstChild.removeEventListener('keypress', firstChildKeypressListener); 12821 }); 12822 } 12823 } 12824 }; 12825 } 12826 mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"]; 12827 12828 /* 12829 * @private 12830 * @ngdoc controller 12831 * @name MdListController 12832 * @module material.components.list 12833 * 12834 */ 12835 function MdListController($scope, $element, $mdListInkRipple) { 12836 var ctrl = this; 12837 ctrl.attachRipple = attachRipple; 12838 12839 function attachRipple (scope, element) { 12840 var options = {}; 12841 $mdListInkRipple.attach(scope, element, options); 12842 } 12843 } 12844 MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"]; 12845 12846 12847 })(); 12848 (function(){ 12849 "use strict"; 12850 12851 /** 12852 * @ngdoc module 12853 * @name material.components.menu-bar 12854 */ 12855 12856 angular.module('material.components.menuBar', [ 12857 'material.core', 12858 'material.components.menu' 12859 ]); 12860 12861 })(); 12862 (function(){ 12863 "use strict"; 12864 12865 /** 12866 * @ngdoc module 12867 * @name material.components.progressCircular 12868 * @description Circular Progress module! 12869 */ 12870 angular.module('material.components.progressCircular', [ 12871 'material.core' 12872 ]) 12873 .directive('mdProgressCircular', MdProgressCircularDirective); 12874 12875 /** 12876 * @ngdoc directive 12877 * @name mdProgressCircular 12878 * @module material.components.progressCircular 12879 * @restrict E 12880 * 12881 * @description 12882 * The circular progress directive is used to make loading content in your app as delightful and 12883 * painless as possible by minimizing the amount of visual change a user sees before they can view 12884 * and interact with content. 12885 * 12886 * For operations where the percentage of the operation completed can be determined, use a 12887 * determinate indicator. They give users a quick sense of how long an operation will take. 12888 * 12889 * For operations where the user is asked to wait a moment while something finishes up, and it’s 12890 * not necessary to expose what's happening behind the scenes and how long it will take, use an 12891 * indeterminate indicator. 12892 * 12893 * @param {string} md-mode Select from one of two modes: **'determinate'** and **'indeterminate'**. 12894 * 12895 * Note: if the `md-mode` value is set as undefined or specified as not 1 of the two (2) valid modes, then `.ng-hide` 12896 * will be auto-applied as a style to the component. 12897 * 12898 * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. 12899 * If `value=""` is also specified, however, then `md-mode="determinate"` would be auto-injected instead. 12900 * @param {number=} value In determinate mode, this number represents the percentage of the 12901 * circular progress. Default: 0 12902 * @param {number=} md-diameter This specifies the diameter of the circular progress. The value 12903 * may be a percentage (eg '25%') or a pixel-size value (eg '48'). If this attribute is 12904 * not present then a default value of '48px' is assumed. 12905 * 12906 * @usage 12907 * <hljs lang="html"> 12908 * <md-progress-circular md-mode="determinate" value="..."></md-progress-circular> 12909 * 12910 * <md-progress-circular md-mode="determinate" ng-value="..."></md-progress-circular> 12911 * 12912 * <md-progress-circular md-mode="determinate" value="..." md-diameter="100"></md-progress-circular> 12913 * 12914 * <md-progress-circular md-mode="indeterminate"></md-progress-circular> 12915 * </hljs> 12916 */ 12917 function MdProgressCircularDirective($mdTheming, $mdUtil, $log) { 12918 var DEFAULT_PROGRESS_SIZE = 100; 12919 var DEFAULT_SCALING = 0.5; 12920 12921 var MODE_DETERMINATE = "determinate", 12922 MODE_INDETERMINATE = "indeterminate"; 12923 12924 12925 return { 12926 restrict: 'E', 12927 scope : true, 12928 template: 12929 // The progress 'circle' is composed of two half-circles: the left side and the right 12930 // side. Each side has CSS applied to 'fill-in' the half-circle to the appropriate progress. 12931 '<div class="md-scale-wrapper">' + 12932 '<div class="md-spinner-wrapper">' + 12933 '<div class="md-inner">' + 12934 '<div class="md-gap"></div>' + 12935 '<div class="md-left">' + 12936 '<div class="md-half-circle"></div>' + 12937 '</div>' + 12938 '<div class="md-right">' + 12939 '<div class="md-half-circle"></div>' + 12940 '</div>' + 12941 '</div>' + 12942 '</div>' + 12943 '</div>', 12944 compile: compile 12945 }; 12946 12947 function compile(tElement) { 12948 // The javascript in this file is mainly responsible for setting the correct aria attributes. 12949 // The animation of the progress spinner is done entirely with just CSS. 12950 tElement.attr('aria-valuemin', 0); 12951 tElement.attr('aria-valuemax', 100); 12952 tElement.attr('role', 'progressbar'); 12953 12954 return postLink; 12955 } 12956 12957 function postLink(scope, element, attr) { 12958 $mdTheming(element); 12959 12960 var circle = element; 12961 var spinnerWrapper = angular.element(element.children()[0]); 12962 var lastMode, toVendorCSS = $mdUtil.dom.animator.toCss; 12963 12964 element.attr('md-mode', mode()); 12965 12966 updateScale(); 12967 validateMode(); 12968 watchAttributes(); 12969 12970 /** 12971 * Watch the value and md-mode attributes 12972 */ 12973 function watchAttributes() { 12974 attr.$observe('value', function(value) { 12975 var percentValue = clamp(value); 12976 element.attr('aria-valuenow', percentValue); 12977 12978 if (mode() == MODE_DETERMINATE) { 12979 animateIndicator(percentValue); 12980 } 12981 }); 12982 attr.$observe('mdMode',function(mode){ 12983 switch( mode ) { 12984 case MODE_DETERMINATE: 12985 case MODE_INDETERMINATE: 12986 spinnerWrapper.removeClass('ng-hide'); 12987 if (lastMode) spinnerWrapper.removeClass(lastMode); 12988 spinnerWrapper.addClass( lastMode = "md-mode-" + mode ); 12989 break; 12990 default: 12991 if (lastMode) spinnerWrapper.removeClass( lastMode ); 12992 spinnerWrapper.addClass('ng-hide'); 12993 lastMode = undefined; 12994 break; 12995 } 12996 }); 12997 } 12998 12999 /** 13000 * Update size/scaling of the progress indicator 13001 * Watch the "value" and "md-mode" attributes 13002 */ 13003 function updateScale() { 13004 // set the outer container to the size the user specified 13005 circle.css({ 13006 width: (100 * getDiameterRatio()) + 'px', 13007 height: (100 * getDiameterRatio()) + 'px' 13008 }); 13009 // the internal element is still 100px, so we have to scale it down to match the size 13010 circle.children().eq(0).css(toVendorCSS({ 13011 transform : $mdUtil.supplant('translate(-50%, -50%) scale( {0} )',[getDiameterRatio()]) 13012 })); 13013 } 13014 13015 /** 13016 * Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified 13017 */ 13018 function validateMode() { 13019 if ( angular.isUndefined(attr.mdMode) ) { 13020 var hasValue = angular.isDefined(attr.value); 13021 var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE; 13022 var info = "Auto-adding the missing md-mode='{0}' to the ProgressCircular element"; 13023 13024 $log.debug( $mdUtil.supplant(info, [mode]) ); 13025 13026 element.attr("md-mode",mode); 13027 attr['mdMode'] = mode; 13028 } 13029 } 13030 13031 var leftC, rightC, gap; 13032 13033 /** 13034 * Manually animate the Determinate indicator based on the specified 13035 * percentage value (0-100). 13036 * 13037 * Note: this animation was previously done using SCSS. 13038 * - generated 54K of styles 13039 * - use attribute selectors which had poor performances in IE 13040 */ 13041 function animateIndicator(value) { 13042 if ( !mode() ) return; 13043 13044 leftC = leftC || angular.element(element[0].querySelector('.md-left > .md-half-circle')); 13045 rightC = rightC || angular.element(element[0].querySelector('.md-right > .md-half-circle')); 13046 gap = gap || angular.element(element[0].querySelector('.md-gap')); 13047 13048 var gapStyles = removeEmptyValues({ 13049 borderBottomColor: (value <= 50) ? "transparent !important" : "", 13050 transition: (value <= 50) ? "" : "borderBottomColor 0.1s linear" 13051 }), 13052 leftStyles = removeEmptyValues({ 13053 transition: (value <= 50) ? "transform 0.1s linear" : "", 13054 transform: $mdUtil.supplant("rotate({0}deg)", [value <= 50 ? 135 : (((value - 50) / 50 * 180) + 135)]) 13055 }), 13056 rightStyles = removeEmptyValues({ 13057 transition: (value >= 50) ? "transform 0.1s linear" : "", 13058 transform: $mdUtil.supplant("rotate({0}deg)", [value >= 50 ? 45 : (value / 50 * 180 - 135)]) 13059 }); 13060 13061 leftC.css(toVendorCSS(leftStyles)); 13062 rightC.css(toVendorCSS(rightStyles)); 13063 gap.css(toVendorCSS(gapStyles)); 13064 13065 } 13066 13067 /** 13068 * We will scale the progress circle based on the default diameter. 13069 * 13070 * Determine the diameter percentage (defaults to 100%) 13071 * May be express as float, percentage, or integer 13072 */ 13073 function getDiameterRatio() { 13074 if ( !attr.mdDiameter ) return DEFAULT_SCALING; 13075 13076 var match = /([0-9]*)%/.exec(attr.mdDiameter); 13077 var value = Math.max(0, (match && match[1]/100) || parseFloat(attr.mdDiameter)); 13078 13079 // should return ratio; DEFAULT_PROGRESS_SIZE === 100px is default size 13080 return (value > 1) ? value / DEFAULT_PROGRESS_SIZE : value; 13081 } 13082 13083 /** 13084 * Is the md-mode a valid option? 13085 */ 13086 function mode() { 13087 var value = (attr.mdMode || "").trim(); 13088 if ( value ) { 13089 switch(value) { 13090 case MODE_DETERMINATE : 13091 case MODE_INDETERMINATE : 13092 break; 13093 default: 13094 value = undefined; 13095 break; 13096 } 13097 } 13098 return value; 13099 } 13100 13101 } 13102 13103 /** 13104 * Clamps the value to be between 0 and 100. 13105 * @param {number} value The value to clamp. 13106 * @returns {number} 13107 */ 13108 function clamp(value) { 13109 return Math.max(0, Math.min(value || 0, 100)); 13110 } 13111 13112 function removeEmptyValues(target) { 13113 for (var key in target) { 13114 if (target.hasOwnProperty(key)) { 13115 if ( target[key] == "" ) delete target[key]; 13116 } 13117 } 13118 13119 return target; 13120 } 13121 } 13122 MdProgressCircularDirective.$inject = ["$mdTheming", "$mdUtil", "$log"]; 13123 13124 })(); 13125 (function(){ 13126 "use strict"; 13127 13128 /** 13129 * @ngdoc module 13130 * @name material.components.progressLinear 13131 * @description Linear Progress module! 13132 */ 13133 angular.module('material.components.progressLinear', [ 13134 'material.core' 13135 ]) 13136 .directive('mdProgressLinear', MdProgressLinearDirective); 13137 13138 /** 13139 * @ngdoc directive 13140 * @name mdProgressLinear 13141 * @module material.components.progressLinear 13142 * @restrict E 13143 * 13144 * @description 13145 * The linear progress directive is used to make loading content 13146 * in your app as delightful and painless as possible by minimizing 13147 * the amount of visual change a user sees before they can view 13148 * and interact with content. 13149 * 13150 * Each operation should only be represented by one activity indicator 13151 * For example: one refresh operation should not display both a 13152 * refresh bar and an activity circle. 13153 * 13154 * For operations where the percentage of the operation completed 13155 * can be determined, use a determinate indicator. They give users 13156 * a quick sense of how long an operation will take. 13157 * 13158 * For operations where the user is asked to wait a moment while 13159 * something finishes up, and it’s not necessary to expose what's 13160 * happening behind the scenes and how long it will take, use an 13161 * indeterminate indicator. 13162 * 13163 * @param {string} md-mode Select from one of four modes: determinate, indeterminate, buffer or query. 13164 * 13165 * Note: if the `md-mode` value is set as undefined or specified as 1 of the four (4) valid modes, then `.ng-hide` 13166 * will be auto-applied as a style to the component. 13167 * 13168 * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. If `value=""` is also specified, however, 13169 * then `md-mode="determinate"` would be auto-injected instead. 13170 * @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0 13171 * @param {number=} md-buffer-value In the buffer mode, this number represents the percentage of the secondary progress bar. Default: 0 13172 * 13173 * @usage 13174 * <hljs lang="html"> 13175 * <md-progress-linear md-mode="determinate" value="..."></md-progress-linear> 13176 * 13177 * <md-progress-linear md-mode="determinate" ng-value="..."></md-progress-linear> 13178 * 13179 * <md-progress-linear md-mode="indeterminate"></md-progress-linear> 13180 * 13181 * <md-progress-linear md-mode="buffer" value="..." md-buffer-value="..."></md-progress-linear> 13182 * 13183 * <md-progress-linear md-mode="query"></md-progress-linear> 13184 * </hljs> 13185 */ 13186 function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { 13187 var MODE_DETERMINATE = "determinate", 13188 MODE_INDETERMINATE = "indeterminate", 13189 MODE_BUFFER = "buffer", 13190 MODE_QUERY = "query"; 13191 13192 return { 13193 restrict: 'E', 13194 template: '<div class="md-container">' + 13195 '<div class="md-dashed"></div>' + 13196 '<div class="md-bar md-bar1"></div>' + 13197 '<div class="md-bar md-bar2"></div>' + 13198 '</div>', 13199 compile: compile 13200 }; 13201 13202 function compile(tElement, tAttrs, transclude) { 13203 tElement.attr('aria-valuemin', 0); 13204 tElement.attr('aria-valuemax', 100); 13205 tElement.attr('role', 'progressbar'); 13206 13207 return postLink; 13208 } 13209 function postLink(scope, element, attr) { 13210 $mdTheming(element); 13211 13212 var lastMode, toVendorCSS = $mdUtil.dom.animator.toCss; 13213 var bar1 = angular.element(element[0].querySelector('.md-bar1')), 13214 bar2 = angular.element(element[0].querySelector('.md-bar2')), 13215 container = angular.element(element[0].querySelector('.md-container')); 13216 13217 element.attr('md-mode', mode()); 13218 13219 validateMode(); 13220 watchAttributes(); 13221 13222 /** 13223 * Watch the value, md-buffer-value, and md-mode attributes 13224 */ 13225 function watchAttributes() { 13226 attr.$observe('value', function(value) { 13227 var percentValue = clamp(value); 13228 element.attr('aria-valuenow', percentValue); 13229 13230 if (mode() != MODE_QUERY) animateIndicator(bar2, percentValue); 13231 }); 13232 13233 attr.$observe('mdBufferValue', function(value) { 13234 animateIndicator(bar1, clamp(value)); 13235 }); 13236 13237 attr.$observe('mdMode',function(mode){ 13238 switch( mode ) { 13239 case MODE_QUERY: 13240 case MODE_BUFFER: 13241 case MODE_DETERMINATE: 13242 case MODE_INDETERMINATE: 13243 container.removeClass( 'ng-hide' + ' ' + lastMode ); 13244 container.addClass( lastMode = "md-mode-" + mode ); 13245 break; 13246 default: 13247 if (lastMode) container.removeClass( lastMode ); 13248 container.addClass('ng-hide'); 13249 lastMode = undefined; 13250 break; 13251 } 13252 }); 13253 } 13254 13255 /** 13256 * Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified 13257 */ 13258 function validateMode() { 13259 if ( angular.isUndefined(attr.mdMode) ) { 13260 var hasValue = angular.isDefined(attr.value); 13261 var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE; 13262 var info = "Auto-adding the missing md-mode='{0}' to the ProgressLinear element"; 13263 13264 $log.debug( $mdUtil.supplant(info, [mode]) ); 13265 13266 element.attr("md-mode",mode); 13267 attr['mdMode'] = mode; 13268 } 13269 } 13270 13271 /** 13272 * Is the md-mode a valid option? 13273 */ 13274 function mode() { 13275 var value = (attr.mdMode || "").trim(); 13276 if ( value ) { 13277 switch(value) { 13278 case MODE_DETERMINATE: 13279 case MODE_INDETERMINATE: 13280 case MODE_BUFFER: 13281 case MODE_QUERY: 13282 break; 13283 default: 13284 value = undefined; 13285 break; 13286 } 13287 } 13288 return value; 13289 } 13290 13291 /** 13292 * Manually set CSS to animate the Determinate indicator based on the specified 13293 * percentage value (0-100). 13294 */ 13295 function animateIndicator(target, value) { 13296 if ( !mode() ) return; 13297 13298 var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [ (value-100)/2, value/100 ]); 13299 var styles = toVendorCSS({ transform : to }); 13300 angular.element(target).css( styles ); 13301 } 13302 } 13303 13304 /** 13305 * Clamps the value to be between 0 and 100. 13306 * @param {number} value The value to clamp. 13307 * @returns {number} 13308 */ 13309 function clamp(value) { 13310 return Math.max(0, Math.min(value || 0, 100)); 13311 } 13312 } 13313 MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"]; 13314 13315 13316 })(); 13317 (function(){ 13318 "use strict"; 13319 13320 /** 13321 * @ngdoc module 13322 * @name material.components.radioButton 13323 * @description radioButton module! 13324 */ 13325 angular.module('material.components.radioButton', [ 13326 'material.core' 13327 ]) 13328 .directive('mdRadioGroup', mdRadioGroupDirective) 13329 .directive('mdRadioButton', mdRadioButtonDirective); 13330 13331 /** 13332 * @ngdoc directive 13333 * @module material.components.radioButton 13334 * @name mdRadioGroup 13335 * 13336 * @restrict E 13337 * 13338 * @description 13339 * The `<md-radio-group>` directive identifies a grouping 13340 * container for the 1..n grouped radio buttons; specified using nested 13341 * `<md-radio-button>` tags. 13342 * 13343 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) 13344 * the radio button is in the accent color by default. The primary color palette may be used with 13345 * the `md-primary` class. 13346 * 13347 * Note: `<md-radio-group>` and `<md-radio-button>` handle tabindex differently 13348 * than the native `<input type='radio'>` controls. Whereas the native controls 13349 * force the user to tab through all the radio buttons, `<md-radio-group>` 13350 * is focusable, and by default the `<md-radio-button>`s are not. 13351 * 13352 * @param {string} ng-model Assignable angular expression to data-bind to. 13353 * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects. 13354 * 13355 * @usage 13356 * <hljs lang="html"> 13357 * <md-radio-group ng-model="selected"> 13358 * 13359 * <md-radio-button 13360 * ng-repeat="d in colorOptions" 13361 * ng-value="d.value" aria-label="{{ d.label }}"> 13362 * 13363 * {{ d.label }} 13364 * 13365 * </md-radio-button> 13366 * 13367 * </md-radio-group> 13368 * </hljs> 13369 * 13370 */ 13371 function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) { 13372 RadioGroupController.prototype = createRadioGroupControllerProto(); 13373 13374 return { 13375 restrict: 'E', 13376 controller: ['$element', RadioGroupController], 13377 require: ['mdRadioGroup', '?ngModel'], 13378 link: { pre: linkRadioGroup } 13379 }; 13380 13381 function linkRadioGroup(scope, element, attr, ctrls) { 13382 $mdTheming(element); 13383 var rgCtrl = ctrls[0]; 13384 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); 13385 13386 rgCtrl.init(ngModelCtrl); 13387 13388 scope.mouseActive = false; 13389 element.attr({ 13390 'role': 'radiogroup', 13391 'tabIndex': element.attr('tabindex') || '0' 13392 }) 13393 .on('keydown', keydownListener) 13394 .on('mousedown', function(event) { 13395 scope.mouseActive = true; 13396 $timeout(function() { 13397 scope.mouseActive = false; 13398 }, 100); 13399 }) 13400 .on('focus', function() { 13401 if(scope.mouseActive === false) { rgCtrl.$element.addClass('md-focused'); } 13402 }) 13403 .on('blur', function() { rgCtrl.$element.removeClass('md-focused'); }); 13404 13405 /** 13406 * 13407 */ 13408 function setFocus() { 13409 if (!element.hasClass('md-focused')) { element.addClass('md-focused'); } 13410 } 13411 13412 /** 13413 * 13414 */ 13415 function keydownListener(ev) { 13416 var keyCode = ev.which || ev.keyCode; 13417 13418 // Only listen to events that we originated ourselves 13419 // so that we don't trigger on things like arrow keys in 13420 // inputs. 13421 13422 if (keyCode != $mdConstant.KEY_CODE.ENTER && 13423 ev.currentTarget != ev.target) { 13424 return; 13425 } 13426 13427 switch (keyCode) { 13428 case $mdConstant.KEY_CODE.LEFT_ARROW: 13429 case $mdConstant.KEY_CODE.UP_ARROW: 13430 ev.preventDefault(); 13431 rgCtrl.selectPrevious(); 13432 setFocus(); 13433 break; 13434 13435 case $mdConstant.KEY_CODE.RIGHT_ARROW: 13436 case $mdConstant.KEY_CODE.DOWN_ARROW: 13437 ev.preventDefault(); 13438 rgCtrl.selectNext(); 13439 setFocus(); 13440 break; 13441 13442 case $mdConstant.KEY_CODE.ENTER: 13443 var form = angular.element($mdUtil.getClosest(element[0], 'form')); 13444 if (form.length > 0) { 13445 form.triggerHandler('submit'); 13446 } 13447 break; 13448 } 13449 13450 } 13451 } 13452 13453 function RadioGroupController($element) { 13454 this._radioButtonRenderFns = []; 13455 this.$element = $element; 13456 } 13457 13458 function createRadioGroupControllerProto() { 13459 return { 13460 init: function(ngModelCtrl) { 13461 this._ngModelCtrl = ngModelCtrl; 13462 this._ngModelCtrl.$render = angular.bind(this, this.render); 13463 }, 13464 add: function(rbRender) { 13465 this._radioButtonRenderFns.push(rbRender); 13466 }, 13467 remove: function(rbRender) { 13468 var index = this._radioButtonRenderFns.indexOf(rbRender); 13469 if (index !== -1) { 13470 this._radioButtonRenderFns.splice(index, 1); 13471 } 13472 }, 13473 render: function() { 13474 this._radioButtonRenderFns.forEach(function(rbRender) { 13475 rbRender(); 13476 }); 13477 }, 13478 setViewValue: function(value, eventType) { 13479 this._ngModelCtrl.$setViewValue(value, eventType); 13480 // update the other radio buttons as well 13481 this.render(); 13482 }, 13483 getViewValue: function() { 13484 return this._ngModelCtrl.$viewValue; 13485 }, 13486 selectNext: function() { 13487 return changeSelectedButton(this.$element, 1); 13488 }, 13489 selectPrevious: function() { 13490 return changeSelectedButton(this.$element, -1); 13491 }, 13492 setActiveDescendant: function (radioId) { 13493 this.$element.attr('aria-activedescendant', radioId); 13494 } 13495 }; 13496 } 13497 /** 13498 * Change the radio group's selected button by a given increment. 13499 * If no button is selected, select the first button. 13500 */ 13501 function changeSelectedButton(parent, increment) { 13502 // Coerce all child radio buttons into an array, then wrap then in an iterator 13503 var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true); 13504 13505 if (buttons.count()) { 13506 var validate = function (button) { 13507 // If disabled, then NOT valid 13508 return !angular.element(button).attr("disabled"); 13509 }; 13510 13511 var selected = parent[0].querySelector('md-radio-button.md-checked'); 13512 var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first(); 13513 13514 // Activate radioButton's click listener (triggerHandler won't create a real click event) 13515 angular.element(target).triggerHandler('click'); 13516 13517 13518 } 13519 } 13520 13521 } 13522 mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"]; 13523 13524 /** 13525 * @ngdoc directive 13526 * @module material.components.radioButton 13527 * @name mdRadioButton 13528 * 13529 * @restrict E 13530 * 13531 * @description 13532 * The `<md-radio-button>`directive is the child directive required to be used within `<md-radio-group>` elements. 13533 * 13534 * While similar to the `<input type="radio" ng-model="" value="">` directive, 13535 * the `<md-radio-button>` directive provides ink effects, ARIA support, and 13536 * supports use within named radio groups. 13537 * 13538 * @param {string} ngModel Assignable angular expression to data-bind to. 13539 * @param {string=} ngChange Angular expression to be executed when input changes due to user 13540 * interaction with the input element. 13541 * @param {string} ngValue Angular expression which sets the value to which the expression should 13542 * be set when selected. 13543 * @param {string} value The value to which the expression should be set when selected. 13544 * @param {string=} name Property name of the form under which the control is published. 13545 * @param {string=} aria-label Adds label to radio button for accessibility. 13546 * Defaults to radio button's text. If no text content is available, a warning will be logged. 13547 * 13548 * @usage 13549 * <hljs lang="html"> 13550 * 13551 * <md-radio-button value="1" aria-label="Label 1"> 13552 * Label 1 13553 * </md-radio-button> 13554 * 13555 * <md-radio-button ng-model="color" ng-value="specialValue" aria-label="Green"> 13556 * Green 13557 * </md-radio-button> 13558 * 13559 * </hljs> 13560 * 13561 */ 13562 function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) { 13563 13564 var CHECKED_CSS = 'md-checked'; 13565 13566 return { 13567 restrict: 'E', 13568 require: '^mdRadioGroup', 13569 transclude: true, 13570 template: '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' + 13571 '<div class="md-off"></div>' + 13572 '<div class="md-on"></div>' + 13573 '</div>' + 13574 '<div ng-transclude class="md-label"></div>', 13575 link: link 13576 }; 13577 13578 function link(scope, element, attr, rgCtrl) { 13579 var lastChecked; 13580 13581 $mdTheming(element); 13582 configureAria(element, scope); 13583 13584 initialize(); 13585 13586 /** 13587 * 13588 */ 13589 function initialize(controller) { 13590 if ( !rgCtrl ) { 13591 throw 'RadioGroupController not found.'; 13592 } 13593 13594 rgCtrl.add(render); 13595 attr.$observe('value', render); 13596 13597 element 13598 .on('click', listener) 13599 .on('$destroy', function() { 13600 rgCtrl.remove(render); 13601 }); 13602 } 13603 13604 /** 13605 * 13606 */ 13607 function listener(ev) { 13608 if (element[0].hasAttribute('disabled')) return; 13609 13610 scope.$apply(function() { 13611 rgCtrl.setViewValue(attr.value, ev && ev.type); 13612 }); 13613 } 13614 13615 /** 13616 * Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent). 13617 * Update the `aria-activedescendant` attribute. 13618 */ 13619 function render() { 13620 var checked = (rgCtrl.getViewValue() == attr.value); 13621 if (checked === lastChecked) { 13622 return; 13623 } 13624 13625 lastChecked = checked; 13626 element.attr('aria-checked', checked); 13627 13628 if (checked) { 13629 markParentAsChecked(true); 13630 element.addClass(CHECKED_CSS); 13631 13632 rgCtrl.setActiveDescendant(element.attr('id')); 13633 13634 } else { 13635 markParentAsChecked(false); 13636 element.removeClass(CHECKED_CSS); 13637 } 13638 13639 /** 13640 * If the radioButton is inside a div, then add class so highlighting will work... 13641 */ 13642 function markParentAsChecked(addClass ) { 13643 if ( element.parent()[0].nodeName != "MD-RADIO-GROUP") { 13644 element.parent()[ !!addClass ? 'addClass' : 'removeClass'](CHECKED_CSS); 13645 } 13646 13647 } 13648 } 13649 13650 /** 13651 * Inject ARIA-specific attributes appropriate for each radio button 13652 */ 13653 function configureAria( element, scope ){ 13654 scope.ariaId = buildAriaID(); 13655 13656 element.attr({ 13657 'id' : scope.ariaId, 13658 'role' : 'radio', 13659 'aria-checked' : 'false' 13660 }); 13661 13662 $mdAria.expectWithText(element, 'aria-label'); 13663 13664 /** 13665 * Build a unique ID for each radio button that will be used with aria-activedescendant. 13666 * Preserve existing ID if already specified. 13667 * @returns {*|string} 13668 */ 13669 function buildAriaID() { 13670 return attr.id || ( 'radio' + "_" + $mdUtil.nextUid() ); 13671 } 13672 } 13673 } 13674 } 13675 mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"]; 13676 13677 })(); 13678 (function(){ 13679 "use strict"; 13680 13681 /** 13682 * @ngdoc module 13683 * @name material.components.showHide 13684 */ 13685 13686 // Add additional handlers to ng-show and ng-hide that notify directives 13687 // contained within that they should recompute their size. 13688 // These run in addition to Angular's built-in ng-hide and ng-show directives. 13689 angular.module('material.components.showHide', [ 13690 'material.core' 13691 ]) 13692 .directive('ngShow', createDirective('ngShow', true)) 13693 .directive('ngHide', createDirective('ngHide', false)); 13694 13695 13696 function createDirective(name, targetValue) { 13697 return ['$mdUtil', function($mdUtil) { 13698 return { 13699 restrict: 'A', 13700 multiElement: true, 13701 link: function($scope, $element, $attr) { 13702 var unregister = $scope.$on('$md-resize-enable', function() { 13703 unregister(); 13704 13705 $scope.$watch($attr[name], function(value) { 13706 if (!!value === targetValue) { 13707 $mdUtil.nextTick(function() { 13708 $scope.$broadcast('$md-resize'); 13709 }); 13710 $mdUtil.dom.animator.waitTransitionEnd($element).then(function() { 13711 $scope.$broadcast('$md-resize'); 13712 }); 13713 } 13714 }); 13715 }); 13716 } 13717 }; 13718 }]; 13719 } 13720 })(); 13721 (function(){ 13722 "use strict"; 13723 13724 /** 13725 * @ngdoc module 13726 * @name material.components.select 13727 */ 13728 13729 /*************************************************** 13730 13731 ### TODO - POST RC1 ### 13732 - [ ] Abstract placement logic in $mdSelect service to $mdMenu service 13733 13734 ***************************************************/ 13735 13736 var SELECT_EDGE_MARGIN = 8; 13737 var selectNextId = 0; 13738 13739 angular.module('material.components.select', [ 13740 'material.core', 13741 'material.components.backdrop' 13742 ]) 13743 .directive('mdSelect', SelectDirective) 13744 .directive('mdSelectMenu', SelectMenuDirective) 13745 .directive('mdOption', OptionDirective) 13746 .directive('mdOptgroup', OptgroupDirective) 13747 .provider('$mdSelect', SelectProvider); 13748 13749 /** 13750 * @ngdoc directive 13751 * @name mdSelect 13752 * @restrict E 13753 * @module material.components.select 13754 * 13755 * @description Displays a select box, bound to an ng-model. 13756 * 13757 * @param {expression} ng-model The model! 13758 * @param {boolean=} multiple Whether it's multiple. 13759 * @param {expression=} md-on-close Expression to be evaluated when the select is closed. 13760 * @param {expression=} md-on-open Expression to be evaluated when opening the select. 13761 * Will hide the select options and show a spinner until the evaluated promise resolves. 13762 * @param {string=} placeholder Placeholder hint text. 13763 * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or 13764 * explicit label is present. 13765 * @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container` 13766 * element (for custom styling). 13767 * 13768 * @usage 13769 * With a placeholder (label and aria-label are added dynamically) 13770 * <hljs lang="html"> 13771 * <md-input-container> 13772 * <md-select 13773 * ng-model="someModel" 13774 * placeholder="Select a state"> 13775 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option> 13776 * </md-select> 13777 * </md-input-container> 13778 * </hljs> 13779 * 13780 * With an explicit label 13781 * <hljs lang="html"> 13782 * <md-input-container> 13783 * <label>State</label> 13784 * <md-select 13785 * ng-model="someModel"> 13786 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option> 13787 * </md-select> 13788 * </md-input-container> 13789 * </hljs> 13790 * 13791 * ## Selects and object equality 13792 * When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles 13793 * equality. Consider the following example: 13794 * <hljs lang="js"> 13795 * angular.controller('MyCtrl', function($scope) { 13796 * $scope.users = [ 13797 * { id: 1, name: 'Bob' }, 13798 * { id: 2, name: 'Alice' }, 13799 * { id: 3, name: 'Steve' } 13800 * ]; 13801 * $scope.selectedUser = { id: 1, name: 'Bob' }; 13802 * }); 13803 * </hljs> 13804 * <hljs lang="html"> 13805 * <div ng-controller="MyCtrl"> 13806 * <md-select ng-model="selectedUser"> 13807 * <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option> 13808 * </md-select> 13809 * </div> 13810 * </hljs> 13811 * 13812 * At first one might expect that the select should be populated with "Bob" as the selected user. However, 13813 * this is not true. To determine whether something is selected, 13814 * `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`; 13815 * 13816 * Javascript's `==` operator does not check for deep equality (ie. that all properties 13817 * on the object are the same), but instead whether the objects are *the same object in memory*. 13818 * In this case, we have two instances of identical objects, but they exist in memory as unique 13819 * entities. Because of this, the select will have no value populated for a selected user. 13820 * 13821 * To get around this, `ngModelController` provides a `track by` option that allows us to specify a different 13822 * expression which will be used for the equality operator. As such, we can update our `html` to 13823 * make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the `md-select` 13824 * element. This converts our equality expression to be 13825 * `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));` 13826 * which results in Bob being selected as desired. 13827 * 13828 * Working HTML: 13829 * <hljs lang="html"> 13830 * <div ng-controller="MyCtrl"> 13831 * <md-select ng-model="selectedUser" ng-model-options="{trackBy: '$value.id'}"> 13832 * <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option> 13833 * </md-select> 13834 * </div> 13835 * </hljs> 13836 */ 13837 function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $parse) { 13838 return { 13839 restrict: 'E', 13840 require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'], 13841 compile: compile, 13842 controller: function() { 13843 } // empty placeholder controller to be initialized in link 13844 }; 13845 13846 function compile(element, attr) { 13847 // add the select value that will hold our placeholder or selected option value 13848 var valueEl = angular.element('<md-select-value><span></span></md-select-value>'); 13849 valueEl.append('<span class="md-select-icon" aria-hidden="true"></span>'); 13850 valueEl.addClass('md-select-value'); 13851 if (!valueEl[0].hasAttribute('id')) { 13852 valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid()); 13853 } 13854 13855 // There's got to be an md-content inside. If there's not one, let's add it. 13856 if (!element.find('md-content').length) { 13857 element.append(angular.element('<md-content>').append(element.contents())); 13858 } 13859 13860 13861 // Add progress spinner for md-options-loading 13862 if (attr.mdOnOpen) { 13863 13864 // Show progress indicator while loading async 13865 // Use ng-hide for `display:none` so the indicator does not interfere with the options list 13866 element 13867 .find('md-content') 13868 .prepend(angular.element( 13869 '<div>' + 13870 ' <md-progress-circular md-mode="{{progressMode}}" ng-hide="$$loadingAsyncDone" md-diameter="25px"></md-progress-circular>' + 13871 '</div>' 13872 )); 13873 13874 // Hide list [of item options] while loading async 13875 element 13876 .find('md-option') 13877 .attr('ng-show', '$$loadingAsyncDone'); 13878 } 13879 13880 if (attr.name) { 13881 var autofillClone = angular.element('<select class="md-visually-hidden">'); 13882 autofillClone.attr({ 13883 'name': '.' + attr.name, 13884 'ng-model': attr.ngModel, 13885 'aria-hidden': 'true', 13886 'tabindex': '-1' 13887 }); 13888 var opts = element.find('md-option'); 13889 angular.forEach(opts, function(el) { 13890 var newEl = angular.element('<option>' + el.innerHTML + '</option>'); 13891 if (el.hasAttribute('ng-value')) newEl.attr('ng-value', el.getAttribute('ng-value')); 13892 else if (el.hasAttribute('value')) newEl.attr('value', el.getAttribute('value')); 13893 autofillClone.append(newEl); 13894 }); 13895 13896 element.parent().append(autofillClone); 13897 } 13898 13899 // Use everything that's left inside element.contents() as the contents of the menu 13900 var multiple = angular.isDefined(attr.multiple) ? 'multiple' : ''; 13901 var selectTemplate = '' + 13902 '<div class="md-select-menu-container" aria-hidden="true">' + 13903 '<md-select-menu {0}>{1}</md-select-menu>' + 13904 '</div>'; 13905 13906 selectTemplate = $mdUtil.supplant(selectTemplate, [multiple, element.html()]); 13907 element.empty().append(valueEl); 13908 element.append(selectTemplate); 13909 13910 if(!attr.tabindex){ 13911 attr.$set('tabindex', 0); 13912 } 13913 13914 return function postLink(scope, element, attr, ctrls) { 13915 var untouched = true; 13916 var isDisabled, ariaLabelBase; 13917 13918 var containerCtrl = ctrls[0]; 13919 var mdSelectCtrl = ctrls[1]; 13920 var ngModelCtrl = ctrls[2]; 13921 var formCtrl = ctrls[3]; 13922 // grab a reference to the select menu value label 13923 var valueEl = element.find('md-select-value'); 13924 var isReadonly = angular.isDefined(attr.readonly); 13925 13926 if (containerCtrl) { 13927 var isErrorGetter = containerCtrl.isErrorGetter || function() { 13928 return ngModelCtrl.$invalid && ngModelCtrl.$touched; 13929 }; 13930 13931 if (containerCtrl.input) { 13932 throw new Error("<md-input-container> can only have *one* child <input>, <textarea> or <select> element!"); 13933 } 13934 13935 containerCtrl.input = element; 13936 if (!containerCtrl.label) { 13937 $mdAria.expect(element, 'aria-label', element.attr('placeholder')); 13938 } 13939 13940 scope.$watch(isErrorGetter, containerCtrl.setInvalid); 13941 } 13942 13943 var selectContainer, selectScope, selectMenuCtrl; 13944 13945 findSelectContainer(); 13946 $mdTheming(element); 13947 13948 if (attr.name && formCtrl) { 13949 var selectEl = element.parent()[0].querySelector('select[name=".' + attr.name + '"]'); 13950 $mdUtil.nextTick(function() { 13951 var controller = angular.element(selectEl).controller('ngModel'); 13952 if (controller) { 13953 formCtrl.$removeControl(controller); 13954 } 13955 }); 13956 } 13957 13958 if (formCtrl && angular.isDefined(attr.multiple)) { 13959 $mdUtil.nextTick(function() { 13960 var hasModelValue = ngModelCtrl.$modelValue || ngModelCtrl.$viewValue; 13961 if (hasModelValue) { 13962 formCtrl.$setPristine(); 13963 } 13964 }); 13965 } 13966 13967 var originalRender = ngModelCtrl.$render; 13968 ngModelCtrl.$render = function() { 13969 originalRender(); 13970 syncLabelText(); 13971 syncAriaLabel(); 13972 inputCheckValue(); 13973 }; 13974 13975 13976 attr.$observe('placeholder', ngModelCtrl.$render); 13977 13978 13979 mdSelectCtrl.setLabelText = function(text) { 13980 mdSelectCtrl.setIsPlaceholder(!text); 13981 // Use placeholder attribute, otherwise fallback to the md-input-container label 13982 var tmpPlaceholder = attr.placeholder || (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : ''); 13983 text = text || tmpPlaceholder || ''; 13984 var target = valueEl.children().eq(0); 13985 target.html(text); 13986 }; 13987 13988 mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) { 13989 if (isPlaceholder) { 13990 valueEl.addClass('md-select-placeholder'); 13991 if (containerCtrl && containerCtrl.label) { 13992 containerCtrl.label.addClass('md-placeholder'); 13993 } 13994 } else { 13995 valueEl.removeClass('md-select-placeholder'); 13996 if (containerCtrl && containerCtrl.label) { 13997 containerCtrl.label.removeClass('md-placeholder'); 13998 } 13999 } 14000 }; 14001 14002 if (!isReadonly) { 14003 element 14004 .on('focus', function(ev) { 14005 // only set focus on if we don't currently have a selected value. This avoids the "bounce" 14006 // on the label transition because the focus will immediately switch to the open menu. 14007 if (containerCtrl && containerCtrl.element.hasClass('md-input-has-value')) { 14008 containerCtrl.setFocused(true); 14009 } 14010 }); 14011 14012 // Attach before ngModel's blur listener to stop propagation of blur event 14013 // to prevent from setting $touched. 14014 element.on('blur', function(event) { 14015 if (untouched) { 14016 untouched = false; 14017 if (selectScope.isOpen) { 14018 event.stopImmediatePropagation(); 14019 } 14020 } 14021 14022 if (selectScope.isOpen) return; 14023 containerCtrl && containerCtrl.setFocused(false); 14024 inputCheckValue(); 14025 }); 14026 } 14027 14028 mdSelectCtrl.triggerClose = function() { 14029 $parse(attr.mdOnClose)(scope); 14030 }; 14031 14032 scope.$$postDigest(function() { 14033 initAriaLabel(); 14034 syncLabelText(); 14035 syncAriaLabel(); 14036 }); 14037 14038 function initAriaLabel() { 14039 var labelText = element.attr('aria-label') || element.attr('placeholder'); 14040 if (!labelText && containerCtrl && containerCtrl.label) { 14041 labelText = containerCtrl.label.text(); 14042 } 14043 ariaLabelBase = labelText; 14044 $mdAria.expect(element, 'aria-label', labelText); 14045 } 14046 14047 scope.$watch(selectMenuCtrl.selectedLabels, syncLabelText); 14048 14049 function syncLabelText() { 14050 if (selectContainer) { 14051 selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu'); 14052 mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels()); 14053 } 14054 } 14055 14056 function syncAriaLabel() { 14057 if (!ariaLabelBase) return; 14058 var ariaLabels = selectMenuCtrl.selectedLabels({mode: 'aria'}); 14059 element.attr('aria-label', ariaLabels.length ? ariaLabelBase + ': ' + ariaLabels : ariaLabelBase); 14060 } 14061 14062 var deregisterWatcher; 14063 attr.$observe('ngMultiple', function(val) { 14064 if (deregisterWatcher) deregisterWatcher(); 14065 var parser = $parse(val); 14066 deregisterWatcher = scope.$watch(function() { 14067 return parser(scope); 14068 }, function(multiple, prevVal) { 14069 if (multiple === undefined && prevVal === undefined) return; // assume compiler did a good job 14070 if (multiple) { 14071 element.attr('multiple', 'multiple'); 14072 } else { 14073 element.removeAttr('multiple'); 14074 } 14075 element.attr('aria-multiselectable', multiple ? 'true' : 'false'); 14076 if (selectContainer) { 14077 selectMenuCtrl.setMultiple(multiple); 14078 originalRender = ngModelCtrl.$render; 14079 ngModelCtrl.$render = function() { 14080 originalRender(); 14081 syncLabelText(); 14082 syncAriaLabel(); 14083 inputCheckValue(); 14084 }; 14085 ngModelCtrl.$render(); 14086 } 14087 }); 14088 }); 14089 14090 attr.$observe('disabled', function(disabled) { 14091 if (angular.isString(disabled)) { 14092 disabled = true; 14093 } 14094 // Prevent click event being registered twice 14095 if (isDisabled !== undefined && isDisabled === disabled) { 14096 return; 14097 } 14098 isDisabled = disabled; 14099 if (disabled) { 14100 element 14101 .attr({'aria-disabled': 'true'}) 14102 .removeAttr('tabindex') 14103 .off('click', openSelect) 14104 .off('keydown', handleKeypress); 14105 } else { 14106 element 14107 .attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'}) 14108 .on('click', openSelect) 14109 .on('keydown', handleKeypress); 14110 } 14111 }); 14112 14113 if (!attr.hasOwnProperty('disabled') && !attr.hasOwnProperty('ngDisabled')) { 14114 element.attr({'aria-disabled': 'false'}); 14115 element.on('click', openSelect); 14116 element.on('keydown', handleKeypress); 14117 } 14118 14119 var ariaAttrs = { 14120 role: 'listbox', 14121 'aria-expanded': 'false', 14122 'aria-multiselectable': attr.multiple !== undefined && !attr.ngMultiple ? 'true' : 'false' 14123 }; 14124 14125 if (!element[0].hasAttribute('id')) { 14126 ariaAttrs.id = 'select_' + $mdUtil.nextUid(); 14127 } 14128 14129 var containerId = 'select_container_' + $mdUtil.nextUid(); 14130 selectContainer.attr('id', containerId); 14131 ariaAttrs['aria-owns'] = containerId; 14132 element.attr(ariaAttrs); 14133 14134 scope.$on('$destroy', function() { 14135 $mdSelect 14136 .destroy() 14137 .finally(function() { 14138 if (containerCtrl) { 14139 containerCtrl.setFocused(false); 14140 containerCtrl.setHasValue(false); 14141 containerCtrl.input = null; 14142 } 14143 ngModelCtrl.$setTouched(); 14144 }); 14145 }); 14146 14147 14148 14149 function inputCheckValue() { 14150 // The select counts as having a value if one or more options are selected, 14151 // or if the input's validity state says it has bad input (eg string in a number input) 14152 containerCtrl && containerCtrl.setHasValue(selectMenuCtrl.selectedLabels().length > 0 || (element[0].validity || {}).badInput); 14153 } 14154 14155 function findSelectContainer() { 14156 selectContainer = angular.element( 14157 element[0].querySelector('.md-select-menu-container') 14158 ); 14159 selectScope = scope; 14160 if (attr.mdContainerClass) { 14161 var value = selectContainer[0].getAttribute('class') + ' ' + attr.mdContainerClass; 14162 selectContainer[0].setAttribute('class', value); 14163 } 14164 selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu'); 14165 selectMenuCtrl.init(ngModelCtrl, attr.ngModel); 14166 element.on('$destroy', function() { 14167 selectContainer.remove(); 14168 }); 14169 } 14170 14171 function handleKeypress(e) { 14172 var allowedCodes = [32, 13, 38, 40]; 14173 if (allowedCodes.indexOf(e.keyCode) != -1) { 14174 // prevent page scrolling on interaction 14175 e.preventDefault(); 14176 openSelect(e); 14177 } else { 14178 if (e.keyCode <= 90 && e.keyCode >= 31) { 14179 e.preventDefault(); 14180 var node = selectMenuCtrl.optNodeForKeyboardSearch(e); 14181 if (!node) return; 14182 var optionCtrl = angular.element(node).controller('mdOption'); 14183 if (!selectMenuCtrl.isMultiple) { 14184 selectMenuCtrl.deselect(Object.keys(selectMenuCtrl.selected)[0]); 14185 } 14186 selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value); 14187 selectMenuCtrl.refreshViewValue(); 14188 } 14189 } 14190 } 14191 14192 function openSelect() { 14193 selectScope.isOpen = true; 14194 element.attr('aria-expanded', 'true'); 14195 14196 $mdSelect.show({ 14197 scope: selectScope, 14198 preserveScope: true, 14199 skipCompile: true, 14200 element: selectContainer, 14201 target: element[0], 14202 selectCtrl: mdSelectCtrl, 14203 preserveElement: true, 14204 hasBackdrop: true, 14205 loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false 14206 }).finally(function() { 14207 selectScope.isOpen = false; 14208 element.focus(); 14209 element.attr('aria-expanded', 'false'); 14210 ngModelCtrl.$setTouched(); 14211 }); 14212 } 14213 }; 14214 } 14215 } 14216 SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdTheming", "$mdAria", "$compile", "$parse"]; 14217 14218 function SelectMenuDirective($parse, $mdUtil, $mdTheming) { 14219 14220 SelectMenuController.$inject = ["$scope", "$attrs", "$element"]; 14221 return { 14222 restrict: 'E', 14223 require: ['mdSelectMenu'], 14224 scope: true, 14225 controller: SelectMenuController, 14226 link: {pre: preLink} 14227 }; 14228 14229 // We use preLink instead of postLink to ensure that the select is initialized before 14230 // its child options run postLink. 14231 function preLink(scope, element, attr, ctrls) { 14232 var selectCtrl = ctrls[0]; 14233 14234 $mdTheming(element); 14235 element.on('click', clickListener); 14236 element.on('keypress', keyListener); 14237 14238 function keyListener(e) { 14239 if (e.keyCode == 13 || e.keyCode == 32) { 14240 clickListener(e); 14241 } 14242 } 14243 14244 function clickListener(ev) { 14245 var option = $mdUtil.getClosest(ev.target, 'md-option'); 14246 var optionCtrl = option && angular.element(option).data('$mdOptionController'); 14247 if (!option || !optionCtrl) return; 14248 if (option.hasAttribute('disabled')) { 14249 ev.stopImmediatePropagation(); 14250 return false; 14251 } 14252 14253 var optionHashKey = selectCtrl.hashGetter(optionCtrl.value); 14254 var isSelected = angular.isDefined(selectCtrl.selected[optionHashKey]); 14255 14256 scope.$apply(function() { 14257 if (selectCtrl.isMultiple) { 14258 if (isSelected) { 14259 selectCtrl.deselect(optionHashKey); 14260 } else { 14261 selectCtrl.select(optionHashKey, optionCtrl.value); 14262 } 14263 } else { 14264 if (!isSelected) { 14265 selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]); 14266 selectCtrl.select(optionHashKey, optionCtrl.value); 14267 } 14268 } 14269 selectCtrl.refreshViewValue(); 14270 }); 14271 } 14272 } 14273 14274 function SelectMenuController($scope, $attrs, $element) { 14275 var self = this; 14276 self.isMultiple = angular.isDefined($attrs.multiple); 14277 // selected is an object with keys matching all of the selected options' hashed values 14278 self.selected = {}; 14279 // options is an object with keys matching every option's hash value, 14280 // and values matching every option's controller. 14281 self.options = {}; 14282 14283 $scope.$watchCollection(function() { 14284 return self.options; 14285 }, function() { 14286 self.ngModel.$render(); 14287 }); 14288 14289 var deregisterCollectionWatch; 14290 var defaultIsEmpty; 14291 self.setMultiple = function(isMultiple) { 14292 var ngModel = self.ngModel; 14293 defaultIsEmpty = defaultIsEmpty || ngModel.$isEmpty; 14294 14295 self.isMultiple = isMultiple; 14296 if (deregisterCollectionWatch) deregisterCollectionWatch(); 14297 14298 if (self.isMultiple) { 14299 ngModel.$validators['md-multiple'] = validateArray; 14300 ngModel.$render = renderMultiple; 14301 14302 // watchCollection on the model because by default ngModel only watches the model's 14303 // reference. This allowed the developer to also push and pop from their array. 14304 $scope.$watchCollection(self.modelBinding, function(value) { 14305 if (validateArray(value)) renderMultiple(value); 14306 self.ngModel.$setPristine(); 14307 }); 14308 14309 ngModel.$isEmpty = function(value) { 14310 return !value || value.length === 0; 14311 }; 14312 } else { 14313 delete ngModel.$validators['md-multiple']; 14314 ngModel.$render = renderSingular; 14315 } 14316 14317 function validateArray(modelValue, viewValue) { 14318 // If a value is truthy but not an array, reject it. 14319 // If value is undefined/falsy, accept that it's an empty array. 14320 return angular.isArray(modelValue || viewValue || []); 14321 } 14322 }; 14323 14324 var searchStr = ''; 14325 var clearSearchTimeout, optNodes, optText; 14326 var CLEAR_SEARCH_AFTER = 300; 14327 self.optNodeForKeyboardSearch = function(e) { 14328 clearSearchTimeout && clearTimeout(clearSearchTimeout); 14329 clearSearchTimeout = setTimeout(function() { 14330 clearSearchTimeout = undefined; 14331 searchStr = ''; 14332 optText = undefined; 14333 optNodes = undefined; 14334 }, CLEAR_SEARCH_AFTER); 14335 searchStr += String.fromCharCode(e.keyCode); 14336 var search = new RegExp('^' + searchStr, 'i'); 14337 if (!optNodes) { 14338 optNodes = $element.find('md-option'); 14339 optText = new Array(optNodes.length); 14340 angular.forEach(optNodes, function(el, i) { 14341 optText[i] = el.textContent.trim(); 14342 }); 14343 } 14344 for (var i = 0; i < optText.length; ++i) { 14345 if (search.test(optText[i])) { 14346 return optNodes[i]; 14347 } 14348 } 14349 }; 14350 14351 self.init = function(ngModel, binding) { 14352 self.ngModel = ngModel; 14353 self.modelBinding = binding; 14354 14355 // Allow users to provide `ng-model="foo" ng-model-options="{trackBy: 'foo.id'}"` so 14356 // that we can properly compare objects set on the model to the available options 14357 if (ngModel.$options && ngModel.$options.trackBy) { 14358 var trackByLocals = {}; 14359 var trackByParsed = $parse(ngModel.$options.trackBy); 14360 self.hashGetter = function(value, valueScope) { 14361 trackByLocals.$value = value; 14362 return trackByParsed(valueScope || $scope, trackByLocals); 14363 }; 14364 // If the user doesn't provide a trackBy, we automatically generate an id for every 14365 // value passed in 14366 } else { 14367 self.hashGetter = function getHashValue(value) { 14368 if (angular.isObject(value)) { 14369 return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId)); 14370 } 14371 return value; 14372 }; 14373 } 14374 self.setMultiple(self.isMultiple); 14375 }; 14376 14377 self.selectedLabels = function(opts) { 14378 opts = opts || {}; 14379 var mode = opts.mode || 'html'; 14380 var selectedOptionEls = $mdUtil.nodesToArray($element[0].querySelectorAll('md-option[selected]')); 14381 if (selectedOptionEls.length) { 14382 var mapFn; 14383 14384 if (mode == 'html') { 14385 // Map the given element to its innerHTML string. If the element has a child ripple 14386 // container remove it from the HTML string, before returning the string. 14387 mapFn = function(el) { 14388 var html = el.innerHTML; 14389 // Remove the ripple container from the selected option, copying it would cause a CSP violation. 14390 var rippleContainer = el.querySelector('.md-ripple-container'); 14391 return rippleContainer ? html.replace(rippleContainer.outerHTML, '') : html; 14392 }; 14393 } else if (mode == 'aria') { 14394 mapFn = function(el) { return el.hasAttribute('aria-label') ? el.getAttribute('aria-label') : el.textContent; }; 14395 } 14396 return selectedOptionEls.map(mapFn).join(', '); 14397 } else { 14398 return ''; 14399 } 14400 }; 14401 14402 self.select = function(hashKey, hashedValue) { 14403 var option = self.options[hashKey]; 14404 option && option.setSelected(true); 14405 self.selected[hashKey] = hashedValue; 14406 }; 14407 self.deselect = function(hashKey) { 14408 var option = self.options[hashKey]; 14409 option && option.setSelected(false); 14410 delete self.selected[hashKey]; 14411 }; 14412 14413 self.addOption = function(hashKey, optionCtrl) { 14414 if (angular.isDefined(self.options[hashKey])) { 14415 throw new Error('Duplicate md-option values are not allowed in a select. ' + 14416 'Duplicate value "' + optionCtrl.value + '" found.'); 14417 } 14418 self.options[hashKey] = optionCtrl; 14419 14420 // If this option's value was already in our ngModel, go ahead and select it. 14421 if (angular.isDefined(self.selected[hashKey])) { 14422 self.select(hashKey, optionCtrl.value); 14423 self.refreshViewValue(); 14424 } 14425 }; 14426 self.removeOption = function(hashKey) { 14427 delete self.options[hashKey]; 14428 // Don't deselect an option when it's removed - the user's ngModel should be allowed 14429 // to have values that do not match a currently available option. 14430 }; 14431 14432 self.refreshViewValue = function() { 14433 var values = []; 14434 var option; 14435 for (var hashKey in self.selected) { 14436 // If this hashKey has an associated option, push that option's value to the model. 14437 if ((option = self.options[hashKey])) { 14438 values.push(option.value); 14439 } else { 14440 // Otherwise, the given hashKey has no associated option, and we got it 14441 // from an ngModel value at an earlier time. Push the unhashed value of 14442 // this hashKey to the model. 14443 // This allows the developer to put a value in the model that doesn't yet have 14444 // an associated option. 14445 values.push(self.selected[hashKey]); 14446 } 14447 } 14448 var usingTrackBy = self.ngModel.$options && self.ngModel.$options.trackBy; 14449 14450 var newVal = self.isMultiple ? values : values[0]; 14451 var prevVal = self.ngModel.$modelValue; 14452 14453 if (usingTrackBy ? !angular.equals(prevVal, newVal) : prevVal != newVal) { 14454 self.ngModel.$setViewValue(newVal); 14455 self.ngModel.$render(); 14456 } 14457 }; 14458 14459 function renderMultiple() { 14460 var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || []; 14461 if (!angular.isArray(newSelectedValues)) return; 14462 14463 var oldSelected = Object.keys(self.selected); 14464 14465 var newSelectedHashes = newSelectedValues.map(self.hashGetter); 14466 var deselected = oldSelected.filter(function(hash) { 14467 return newSelectedHashes.indexOf(hash) === -1; 14468 }); 14469 14470 deselected.forEach(self.deselect); 14471 newSelectedHashes.forEach(function(hashKey, i) { 14472 self.select(hashKey, newSelectedValues[i]); 14473 }); 14474 } 14475 14476 function renderSingular() { 14477 var value = self.ngModel.$viewValue || self.ngModel.$modelValue; 14478 Object.keys(self.selected).forEach(self.deselect); 14479 self.select(self.hashGetter(value), value); 14480 } 14481 } 14482 14483 } 14484 SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdTheming"]; 14485 14486 function OptionDirective($mdButtonInkRipple, $mdUtil) { 14487 14488 OptionController.$inject = ["$element"]; 14489 return { 14490 restrict: 'E', 14491 require: ['mdOption', '^^mdSelectMenu'], 14492 controller: OptionController, 14493 compile: compile 14494 }; 14495 14496 function compile(element, attr) { 14497 // Manual transclusion to avoid the extra inner <span> that ng-transclude generates 14498 element.append(angular.element('<div class="md-text">').append(element.contents())); 14499 14500 element.attr('tabindex', attr.tabindex || '0'); 14501 return postLink; 14502 } 14503 14504 function postLink(scope, element, attr, ctrls) { 14505 var optionCtrl = ctrls[0]; 14506 var selectCtrl = ctrls[1]; 14507 14508 if (angular.isDefined(attr.ngValue)) { 14509 scope.$watch(attr.ngValue, setOptionValue); 14510 } else if (angular.isDefined(attr.value)) { 14511 setOptionValue(attr.value); 14512 } else { 14513 scope.$watch(function() { 14514 return element.text().trim(); 14515 }, setOptionValue); 14516 } 14517 14518 attr.$observe('disabled', function(disabled) { 14519 if (disabled) { 14520 element.attr('tabindex', '-1'); 14521 } else { 14522 element.attr('tabindex', '0'); 14523 } 14524 }); 14525 14526 scope.$$postDigest(function() { 14527 attr.$observe('selected', function(selected) { 14528 if (!angular.isDefined(selected)) return; 14529 if (typeof selected == 'string') selected = true; 14530 if (selected) { 14531 if (!selectCtrl.isMultiple) { 14532 selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]); 14533 } 14534 selectCtrl.select(optionCtrl.hashKey, optionCtrl.value); 14535 } else { 14536 selectCtrl.deselect(optionCtrl.hashKey); 14537 } 14538 selectCtrl.refreshViewValue(); 14539 }); 14540 }); 14541 14542 $mdButtonInkRipple.attach(scope, element); 14543 configureAria(); 14544 14545 function setOptionValue(newValue, oldValue, prevAttempt) { 14546 if (!selectCtrl.hashGetter) { 14547 if (!prevAttempt) { 14548 scope.$$postDigest(function() { 14549 setOptionValue(newValue, oldValue, true); 14550 }); 14551 } 14552 return; 14553 } 14554 var oldHashKey = selectCtrl.hashGetter(oldValue, scope); 14555 var newHashKey = selectCtrl.hashGetter(newValue, scope); 14556 14557 optionCtrl.hashKey = newHashKey; 14558 optionCtrl.value = newValue; 14559 14560 selectCtrl.removeOption(oldHashKey, optionCtrl); 14561 selectCtrl.addOption(newHashKey, optionCtrl); 14562 } 14563 14564 scope.$on('$destroy', function() { 14565 selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl); 14566 }); 14567 14568 function configureAria() { 14569 var ariaAttrs = { 14570 'role': 'option', 14571 'aria-selected': 'false' 14572 }; 14573 14574 if (!element[0].hasAttribute('id')) { 14575 ariaAttrs.id = 'select_option_' + $mdUtil.nextUid(); 14576 } 14577 element.attr(ariaAttrs); 14578 } 14579 } 14580 14581 function OptionController($element) { 14582 this.selected = false; 14583 this.setSelected = function(isSelected) { 14584 if (isSelected && !this.selected) { 14585 $element.attr({ 14586 'selected': 'selected', 14587 'aria-selected': 'true' 14588 }); 14589 } else if (!isSelected && this.selected) { 14590 $element.removeAttr('selected'); 14591 $element.attr('aria-selected', 'false'); 14592 } 14593 this.selected = isSelected; 14594 }; 14595 } 14596 14597 } 14598 OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"]; 14599 14600 function OptgroupDirective() { 14601 return { 14602 restrict: 'E', 14603 compile: compile 14604 }; 14605 function compile(el, attrs) { 14606 var labelElement = el.find('label'); 14607 if (!labelElement.length) { 14608 labelElement = angular.element('<label>'); 14609 el.prepend(labelElement); 14610 } 14611 labelElement.addClass('md-container-ignore'); 14612 if (attrs.label) labelElement.text(attrs.label); 14613 } 14614 } 14615 14616 function SelectProvider($$interimElementProvider) { 14617 selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$mdUtil", "$window", "$q", "$$rAF", "$animateCss", "$animate", "$document"]; 14618 return $$interimElementProvider('$mdSelect') 14619 .setDefaults({ 14620 methods: ['target'], 14621 options: selectDefaultOptions 14622 }); 14623 14624 /* @ngInject */ 14625 function selectDefaultOptions($mdSelect, $mdConstant, $mdUtil, $window, $q, $$rAF, $animateCss, $animate, $document) { 14626 var ERRROR_TARGET_EXPECTED = "$mdSelect.show() expected a target element in options.target but got '{0}'!"; 14627 var animator = $mdUtil.dom.animator; 14628 14629 return { 14630 parent: 'body', 14631 themable: true, 14632 onShow: onShow, 14633 onRemove: onRemove, 14634 hasBackdrop: true, 14635 disableParentScroll: true 14636 }; 14637 14638 /** 14639 * Interim-element onRemove logic.... 14640 */ 14641 function onRemove(scope, element, opts) { 14642 opts = opts || { }; 14643 opts.cleanupInteraction(); 14644 opts.cleanupResizing(); 14645 opts.hideBackdrop(); 14646 14647 // For navigation $destroy events, do a quick, non-animated removal, 14648 // but for normal closes (from clicks, etc) animate the removal 14649 14650 return (opts.$destroy === true) ? cleanElement() : animateRemoval().then( cleanElement ); 14651 14652 /** 14653 * For normal closes (eg clicks), animate the removal. 14654 * For forced closes (like $destroy events from navigation), 14655 * skip the animations 14656 */ 14657 function animateRemoval() { 14658 return $animateCss(element, {addClass: 'md-leave'}).start(); 14659 } 14660 14661 /** 14662 * Restore the element to a closed state 14663 */ 14664 function cleanElement() { 14665 14666 element.removeClass('md-active'); 14667 element.attr('aria-hidden', 'true'); 14668 element[0].style.display = 'none'; 14669 14670 announceClosed(opts); 14671 14672 if (!opts.$destroy && opts.restoreFocus) { 14673 opts.target.focus(); 14674 } 14675 } 14676 14677 } 14678 14679 /** 14680 * Interim-element onShow logic.... 14681 */ 14682 function onShow(scope, element, opts) { 14683 14684 watchAsyncLoad(); 14685 sanitizeAndConfigure(scope, opts); 14686 14687 opts.hideBackdrop = showBackdrop(scope, element, opts); 14688 14689 return showDropDown(scope, element, opts) 14690 .then(function(response) { 14691 element.attr('aria-hidden', 'false'); 14692 opts.alreadyOpen = true; 14693 opts.cleanupInteraction = activateInteraction(); 14694 opts.cleanupResizing = activateResizing(); 14695 14696 return response; 14697 }, opts.hideBackdrop); 14698 14699 // ************************************ 14700 // Closure Functions 14701 // ************************************ 14702 14703 /** 14704 * Attach the select DOM element(s) and animate to the correct positions 14705 * and scalings... 14706 */ 14707 function showDropDown(scope, element, opts) { 14708 opts.parent.append(element); 14709 14710 return $q(function(resolve, reject) { 14711 14712 try { 14713 14714 $animateCss(element, {removeClass: 'md-leave', duration: 0}) 14715 .start() 14716 .then(positionAndFocusMenu) 14717 .then(resolve); 14718 14719 } catch (e) { 14720 reject(e); 14721 } 14722 14723 }); 14724 } 14725 14726 /** 14727 * Initialize container and dropDown menu positions/scale, then animate 14728 * to show... and autoFocus. 14729 */ 14730 function positionAndFocusMenu() { 14731 return $q(function(resolve) { 14732 if (opts.isRemoved) return $q.reject(false); 14733 14734 var info = calculateMenuPositions(scope, element, opts); 14735 14736 info.container.element.css(animator.toCss(info.container.styles)); 14737 info.dropDown.element.css(animator.toCss(info.dropDown.styles)); 14738 14739 $$rAF(function() { 14740 element.addClass('md-active'); 14741 info.dropDown.element.css(animator.toCss({transform: ''})); 14742 14743 autoFocus(opts.focusedNode); 14744 resolve(); 14745 }); 14746 14747 }); 14748 } 14749 14750 /** 14751 * Show modal backdrop element... 14752 */ 14753 function showBackdrop(scope, element, options) { 14754 14755 // If we are not within a dialog... 14756 if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) { 14757 // !! DO this before creating the backdrop; since disableScrollAround() 14758 // configures the scroll offset; which is used by mdBackDrop postLink() 14759 options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent); 14760 } else { 14761 options.disableParentScroll = false; 14762 } 14763 14764 if (options.hasBackdrop) { 14765 // Override duration to immediately show invisible backdrop 14766 options.backdrop = $mdUtil.createBackdrop(scope, "md-select-backdrop md-click-catcher"); 14767 $animate.enter(options.backdrop, $document[0].body, null, {duration: 0}); 14768 } 14769 14770 /** 14771 * Hide modal backdrop element... 14772 */ 14773 return function hideBackdrop() { 14774 if (options.backdrop) options.backdrop.remove(); 14775 if (options.disableParentScroll) options.restoreScroll(); 14776 14777 delete options.restoreScroll; 14778 }; 14779 } 14780 14781 /** 14782 * 14783 */ 14784 function autoFocus(focusedNode) { 14785 if (focusedNode && !focusedNode.hasAttribute('disabled')) { 14786 focusedNode.focus(); 14787 } 14788 } 14789 14790 /** 14791 * Check for valid opts and set some sane defaults 14792 */ 14793 function sanitizeAndConfigure(scope, options) { 14794 var selectEl = element.find('md-select-menu'); 14795 14796 if (!options.target) { 14797 throw new Error($mdUtil.supplant(ERRROR_TARGET_EXPECTED, [options.target])); 14798 } 14799 14800 angular.extend(options, { 14801 isRemoved: false, 14802 target: angular.element(options.target), //make sure it's not a naked dom node 14803 parent: angular.element(options.parent), 14804 selectEl: selectEl, 14805 contentEl: element.find('md-content'), 14806 optionNodes: selectEl[0].getElementsByTagName('md-option') 14807 }); 14808 } 14809 14810 /** 14811 * Configure various resize listeners for screen changes 14812 */ 14813 function activateResizing() { 14814 var debouncedOnResize = (function(scope, target, options) { 14815 14816 return function() { 14817 if (options.isRemoved) return; 14818 14819 var updates = calculateMenuPositions(scope, target, options); 14820 var container = updates.container; 14821 var dropDown = updates.dropDown; 14822 14823 container.element.css(animator.toCss(container.styles)); 14824 dropDown.element.css(animator.toCss(dropDown.styles)); 14825 }; 14826 14827 })(scope, element, opts); 14828 14829 var window = angular.element($window); 14830 window.on('resize', debouncedOnResize); 14831 window.on('orientationchange', debouncedOnResize); 14832 14833 // Publish deactivation closure... 14834 return function deactivateResizing() { 14835 14836 // Disable resizing handlers 14837 window.off('resize', debouncedOnResize); 14838 window.off('orientationchange', debouncedOnResize); 14839 }; 14840 } 14841 14842 /** 14843 * If asynchronously loading, watch and update internal 14844 * '$$loadingAsyncDone' flag 14845 */ 14846 function watchAsyncLoad() { 14847 if (opts.loadingAsync && !opts.isRemoved) { 14848 scope.$$loadingAsyncDone = false; 14849 scope.progressMode = 'indeterminate'; 14850 14851 $q.when(opts.loadingAsync) 14852 .then(function() { 14853 scope.$$loadingAsyncDone = true; 14854 scope.progressMode = ''; 14855 delete opts.loadingAsync; 14856 }).then(function() { 14857 $$rAF(positionAndFocusMenu); 14858 }); 14859 } 14860 } 14861 14862 /** 14863 * 14864 */ 14865 function activateInteraction() { 14866 if (opts.isRemoved) return; 14867 14868 var dropDown = opts.selectEl; 14869 var selectCtrl = dropDown.controller('mdSelectMenu') || {}; 14870 14871 element.addClass('md-clickable'); 14872 14873 // Close on backdrop click 14874 opts.backdrop && opts.backdrop.on('click', onBackdropClick); 14875 14876 // Escape to close 14877 // Cycling of options, and closing on enter 14878 dropDown.on('keydown', onMenuKeyDown); 14879 dropDown.on('click', checkCloseMenu); 14880 14881 return function cleanupInteraction() { 14882 opts.backdrop && opts.backdrop.off('click', onBackdropClick); 14883 dropDown.off('keydown', onMenuKeyDown); 14884 dropDown.off('click', checkCloseMenu); 14885 14886 element.removeClass('md-clickable'); 14887 opts.isRemoved = true; 14888 }; 14889 14890 // ************************************ 14891 // Closure Functions 14892 // ************************************ 14893 14894 function onBackdropClick(e) { 14895 e.preventDefault(); 14896 e.stopPropagation(); 14897 opts.restoreFocus = false; 14898 $mdUtil.nextTick($mdSelect.hide, true); 14899 } 14900 14901 function onMenuKeyDown(ev) { 14902 var keyCodes = $mdConstant.KEY_CODE; 14903 ev.preventDefault(); 14904 ev.stopPropagation(); 14905 14906 switch (ev.keyCode) { 14907 case keyCodes.UP_ARROW: 14908 return focusPrevOption(); 14909 case keyCodes.DOWN_ARROW: 14910 return focusNextOption(); 14911 case keyCodes.SPACE: 14912 case keyCodes.ENTER: 14913 var option = $mdUtil.getClosest(ev.target, 'md-option'); 14914 if (option) { 14915 dropDown.triggerHandler({ 14916 type: 'click', 14917 target: option 14918 }); 14919 ev.preventDefault(); 14920 } 14921 checkCloseMenu(ev); 14922 break; 14923 case keyCodes.TAB: 14924 case keyCodes.ESCAPE: 14925 ev.stopPropagation(); 14926 ev.preventDefault(); 14927 opts.restoreFocus = true; 14928 $mdUtil.nextTick($mdSelect.hide, true); 14929 break; 14930 default: 14931 if (ev.keyCode >= 31 && ev.keyCode <= 90) { 14932 var optNode = dropDown.controller('mdSelectMenu').optNodeForKeyboardSearch(ev); 14933 opts.focusedNode = optNode || opts.focusedNode; 14934 optNode && optNode.focus(); 14935 } 14936 } 14937 } 14938 14939 function focusOption(direction) { 14940 var optionsArray = $mdUtil.nodesToArray(opts.optionNodes); 14941 var index = optionsArray.indexOf(opts.focusedNode); 14942 14943 var newOption; 14944 14945 do { 14946 if (index === -1) { 14947 // We lost the previously focused element, reset to first option 14948 index = 0; 14949 } else if (direction === 'next' && index < optionsArray.length - 1) { 14950 index++; 14951 } else if (direction === 'prev' && index > 0) { 14952 index--; 14953 } 14954 newOption = optionsArray[index]; 14955 if (newOption.hasAttribute('disabled')) newOption = undefined; 14956 } while (!newOption && index < optionsArray.length - 1 && index > 0); 14957 newOption && newOption.focus(); 14958 opts.focusedNode = newOption; 14959 } 14960 14961 function focusNextOption() { 14962 focusOption('next'); 14963 } 14964 14965 function focusPrevOption() { 14966 focusOption('prev'); 14967 } 14968 14969 function checkCloseMenu(ev) { 14970 if (ev && ( ev.type == 'click') && (ev.currentTarget != dropDown[0])) return; 14971 if ( mouseOnScrollbar() ) return; 14972 14973 var option = $mdUtil.getClosest(ev.target, 'md-option'); 14974 if (option && option.hasAttribute && !option.hasAttribute('disabled')) { 14975 ev.preventDefault(); 14976 ev.stopPropagation(); 14977 if (!selectCtrl.isMultiple) { 14978 opts.restoreFocus = true; 14979 14980 $mdUtil.nextTick(function () { 14981 $mdSelect.hide(selectCtrl.ngModel.$viewValue); 14982 }, true); 14983 } 14984 } 14985 /** 14986 * check if the mouseup event was on a scrollbar 14987 */ 14988 function mouseOnScrollbar() { 14989 var clickOnScrollbar = false; 14990 if (ev && (ev.currentTarget.children.length > 0)) { 14991 var child = ev.currentTarget.children[0]; 14992 var hasScrollbar = child.scrollHeight > child.clientHeight; 14993 if (hasScrollbar && child.children.length > 0) { 14994 var relPosX = ev.pageX - ev.currentTarget.getBoundingClientRect().left; 14995 if (relPosX > child.querySelector('md-option').offsetWidth) 14996 clickOnScrollbar = true; 14997 } 14998 } 14999 return clickOnScrollbar; 15000 } 15001 } 15002 } 15003 15004 } 15005 15006 /** 15007 * To notify listeners that the Select menu has closed, 15008 * trigger the [optional] user-defined expression 15009 */ 15010 function announceClosed(opts) { 15011 var mdSelect = opts.selectCtrl; 15012 if (mdSelect) { 15013 var menuController = opts.selectEl.controller('mdSelectMenu'); 15014 mdSelect.setLabelText(menuController.selectedLabels()); 15015 mdSelect.triggerClose(); 15016 } 15017 } 15018 15019 15020 /** 15021 * Calculate the 15022 */ 15023 function calculateMenuPositions(scope, element, opts) { 15024 var 15025 containerNode = element[0], 15026 targetNode = opts.target[0].children[0], // target the label 15027 parentNode = $document[0].body, 15028 selectNode = opts.selectEl[0], 15029 contentNode = opts.contentEl[0], 15030 parentRect = parentNode.getBoundingClientRect(), 15031 targetRect = targetNode.getBoundingClientRect(), 15032 shouldOpenAroundTarget = false, 15033 bounds = { 15034 left: parentRect.left + SELECT_EDGE_MARGIN, 15035 top: SELECT_EDGE_MARGIN, 15036 bottom: parentRect.height - SELECT_EDGE_MARGIN, 15037 right: parentRect.width - SELECT_EDGE_MARGIN - ($mdUtil.floatingScrollbars() ? 16 : 0) 15038 }, 15039 spaceAvailable = { 15040 top: targetRect.top - bounds.top, 15041 left: targetRect.left - bounds.left, 15042 right: bounds.right - (targetRect.left + targetRect.width), 15043 bottom: bounds.bottom - (targetRect.top + targetRect.height) 15044 }, 15045 maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2, 15046 selectedNode = selectNode.querySelector('md-option[selected]'), 15047 optionNodes = selectNode.getElementsByTagName('md-option'), 15048 optgroupNodes = selectNode.getElementsByTagName('md-optgroup'), 15049 isScrollable = calculateScrollable(element, contentNode), 15050 centeredNode; 15051 15052 var loading = isPromiseLike(opts.loadingAsync); 15053 if (!loading) { 15054 // If a selected node, center around that 15055 if (selectedNode) { 15056 centeredNode = selectedNode; 15057 // If there are option groups, center around the first option group 15058 } else if (optgroupNodes.length) { 15059 centeredNode = optgroupNodes[0]; 15060 // Otherwise - if we are not loading async - center around the first optionNode 15061 } else if (optionNodes.length) { 15062 centeredNode = optionNodes[0]; 15063 // In case there are no options, center on whatever's in there... (eg progress indicator) 15064 } else { 15065 centeredNode = contentNode.firstElementChild || contentNode; 15066 } 15067 } else { 15068 // If loading, center on progress indicator 15069 centeredNode = contentNode.firstElementChild || contentNode; 15070 } 15071 15072 if (contentNode.offsetWidth > maxWidth) { 15073 contentNode.style['max-width'] = maxWidth + 'px'; 15074 } else { 15075 contentNode.style.maxWidth = null; 15076 } 15077 if (shouldOpenAroundTarget) { 15078 contentNode.style['min-width'] = targetRect.width + 'px'; 15079 } 15080 15081 // Remove padding before we compute the position of the menu 15082 if (isScrollable) { 15083 selectNode.classList.add('md-overflow'); 15084 } 15085 15086 var focusedNode = centeredNode; 15087 if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') { 15088 focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode; 15089 centeredNode = focusedNode; 15090 } 15091 // Cache for autoFocus() 15092 opts.focusedNode = focusedNode; 15093 15094 // Get the selectMenuRect *after* max-width is possibly set above 15095 containerNode.style.display = 'block'; 15096 var selectMenuRect = selectNode.getBoundingClientRect(); 15097 var centeredRect = getOffsetRect(centeredNode); 15098 15099 if (centeredNode) { 15100 var centeredStyle = $window.getComputedStyle(centeredNode); 15101 centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0; 15102 centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0; 15103 } 15104 15105 if (isScrollable) { 15106 var scrollBuffer = contentNode.offsetHeight / 2; 15107 contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer; 15108 15109 if (spaceAvailable.top < scrollBuffer) { 15110 contentNode.scrollTop = Math.min( 15111 centeredRect.top, 15112 contentNode.scrollTop + scrollBuffer - spaceAvailable.top 15113 ); 15114 } else if (spaceAvailable.bottom < scrollBuffer) { 15115 contentNode.scrollTop = Math.max( 15116 centeredRect.top + centeredRect.height - selectMenuRect.height, 15117 contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom 15118 ); 15119 } 15120 } 15121 15122 var left, top, transformOrigin, minWidth; 15123 if (shouldOpenAroundTarget) { 15124 left = targetRect.left; 15125 top = targetRect.top + targetRect.height; 15126 transformOrigin = '50% 0'; 15127 if (top + selectMenuRect.height > bounds.bottom) { 15128 top = targetRect.top - selectMenuRect.height; 15129 transformOrigin = '50% 100%'; 15130 } 15131 } else { 15132 left = (targetRect.left + centeredRect.left - centeredRect.paddingLeft) + 2; 15133 top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 - 15134 centeredRect.top + contentNode.scrollTop) + 2; 15135 15136 transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' + 15137 (centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px'; 15138 15139 minWidth = Math.min(targetRect.width + centeredRect.paddingLeft + centeredRect.paddingRight, maxWidth); 15140 } 15141 15142 // Keep left and top within the window 15143 var containerRect = containerNode.getBoundingClientRect(); 15144 var scaleX = Math.round(100 * Math.min(targetRect.width / selectMenuRect.width, 1.0)) / 100; 15145 var scaleY = Math.round(100 * Math.min(targetRect.height / selectMenuRect.height, 1.0)) / 100; 15146 15147 return { 15148 container: { 15149 element: angular.element(containerNode), 15150 styles: { 15151 left: Math.floor(clamp(bounds.left, left, bounds.right - containerRect.width)), 15152 top: Math.floor(clamp(bounds.top, top, bounds.bottom - containerRect.height)), 15153 'min-width': minWidth 15154 } 15155 }, 15156 dropDown: { 15157 element: angular.element(selectNode), 15158 styles: { 15159 transformOrigin: transformOrigin, 15160 transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : "" 15161 } 15162 } 15163 }; 15164 15165 } 15166 15167 } 15168 15169 function isPromiseLike(obj) { 15170 return obj && angular.isFunction(obj.then); 15171 } 15172 15173 function clamp(min, n, max) { 15174 return Math.max(min, Math.min(n, max)); 15175 } 15176 15177 function getOffsetRect(node) { 15178 return node ? { 15179 left: node.offsetLeft, 15180 top: node.offsetTop, 15181 width: node.offsetWidth, 15182 height: node.offsetHeight 15183 } : {left: 0, top: 0, width: 0, height: 0}; 15184 } 15185 15186 function calculateScrollable(element, contentNode) { 15187 var isScrollable = false; 15188 15189 try { 15190 var oldDisplay = element[0].style.display; 15191 15192 // Set the element's display to block so that this calculation is correct 15193 element[0].style.display = 'block'; 15194 15195 isScrollable = contentNode.scrollHeight > contentNode.offsetHeight; 15196 15197 // Reset it back afterwards 15198 element[0].style.display = oldDisplay; 15199 } finally { 15200 // Nothing to do 15201 } 15202 return isScrollable; 15203 } 15204 } 15205 SelectProvider.$inject = ["$$interimElementProvider"]; 15206 15207 15208 })(); 15209 (function(){ 15210 "use strict"; 15211 15212 /** 15213 * @ngdoc module 15214 * @name material.components.sidenav 15215 * 15216 * @description 15217 * A Sidenav QP component. 15218 */ 15219 angular 15220 .module('material.components.sidenav', [ 15221 'material.core', 15222 'material.components.backdrop' 15223 ]) 15224 .factory('$mdSidenav', SidenavService ) 15225 .directive('mdSidenav', SidenavDirective) 15226 .directive('mdSidenavFocus', SidenavFocusDirective) 15227 .controller('$mdSidenavController', SidenavController); 15228 15229 15230 /** 15231 * @ngdoc service 15232 * @name $mdSidenav 15233 * @module material.components.sidenav 15234 * 15235 * @description 15236 * `$mdSidenav` makes it easy to interact with multiple sidenavs 15237 * in an app. 15238 * 15239 * @usage 15240 * <hljs lang="js"> 15241 * // Async lookup for sidenav instance; will resolve when the instance is available 15242 * $mdSidenav(componentId).then(function(instance) { 15243 * $log.debug( componentId + "is now ready" ); 15244 * }); 15245 * // Async toggle the given sidenav; 15246 * // when instance is known ready and lazy lookup is not needed. 15247 * $mdSidenav(componentId) 15248 * .toggle() 15249 * .then(function(){ 15250 * $log.debug('toggled'); 15251 * }); 15252 * // Async open the given sidenav 15253 * $mdSidenav(componentId) 15254 * .open() 15255 * .then(function(){ 15256 * $log.debug('opened'); 15257 * }); 15258 * // Async close the given sidenav 15259 * $mdSidenav(componentId) 15260 * .close() 15261 * .then(function(){ 15262 * $log.debug('closed'); 15263 * }); 15264 * // Sync check to see if the specified sidenav is set to be open 15265 * $mdSidenav(componentId).isOpen(); 15266 * // Sync check to whether given sidenav is locked open 15267 * // If this is true, the sidenav will be open regardless of close() 15268 * $mdSidenav(componentId).isLockedOpen(); 15269 * </hljs> 15270 */ 15271 function SidenavService($mdComponentRegistry, $q) { 15272 return function(handle) { 15273 15274 // Lookup the controller instance for the specified sidNav instance 15275 var self; 15276 var errorMsg = "SideNav '" + handle + "' is not available!"; 15277 var instance = $mdComponentRegistry.get(handle); 15278 15279 if(!instance) { 15280 $mdComponentRegistry.notFoundError(handle); 15281 } 15282 15283 return self = { 15284 // ----------------- 15285 // Sync methods 15286 // ----------------- 15287 isOpen: function() { 15288 return instance && instance.isOpen(); 15289 }, 15290 isLockedOpen: function() { 15291 return instance && instance.isLockedOpen(); 15292 }, 15293 // ----------------- 15294 // Async methods 15295 // ----------------- 15296 toggle: function() { 15297 return instance ? instance.toggle() : $q.reject(errorMsg); 15298 }, 15299 open: function() { 15300 return instance ? instance.open() : $q.reject(errorMsg); 15301 }, 15302 close: function() { 15303 return instance ? instance.close() : $q.reject(errorMsg); 15304 }, 15305 then : function( callbackFn ) { 15306 var promise = instance ? $q.when(instance) : waitForInstance(); 15307 return promise.then( callbackFn || angular.noop ); 15308 } 15309 }; 15310 15311 /** 15312 * Deferred lookup of component instance using $component registry 15313 */ 15314 function waitForInstance() { 15315 return $mdComponentRegistry 15316 .when(handle) 15317 .then(function( it ){ 15318 instance = it; 15319 return it; 15320 }); 15321 } 15322 }; 15323 } 15324 SidenavService.$inject = ["$mdComponentRegistry", "$q"]; 15325 /** 15326 * @ngdoc directive 15327 * @name mdSidenavFocus 15328 * @module material.components.sidenav 15329 * 15330 * @restrict A 15331 * 15332 * @description 15333 * `mdSidenavFocus` provides a way to specify the focused element when a sidenav opens. 15334 * This is completely optional, as the sidenav itself is focused by default. 15335 * 15336 * @usage 15337 * <hljs lang="html"> 15338 * <md-sidenav> 15339 * <form> 15340 * <md-input-container> 15341 * <label for="testInput">Label</label> 15342 * <input id="testInput" type="text" md-sidenav-focus> 15343 * </md-input-container> 15344 * </form> 15345 * </md-sidenav> 15346 * </hljs> 15347 **/ 15348 function SidenavFocusDirective() { 15349 return { 15350 restrict: 'A', 15351 require: '^mdSidenav', 15352 link: function(scope, element, attr, sidenavCtrl) { 15353 // @see $mdUtil.findFocusTarget(...) 15354 } 15355 }; 15356 } 15357 /** 15358 * @ngdoc directive 15359 * @name mdSidenav 15360 * @module material.components.sidenav 15361 * @restrict E 15362 * 15363 * @description 15364 * 15365 * A Sidenav component that can be opened and closed programatically. 15366 * 15367 * By default, upon opening it will slide out on top of the main content area. 15368 * 15369 * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default. 15370 * It can be overridden with the `md-autofocus` directive on the child element you want focused. 15371 * 15372 * @usage 15373 * <hljs lang="html"> 15374 * <div layout="row" ng-controller="MyController"> 15375 * <md-sidenav md-component-id="left" class="md-sidenav-left"> 15376 * Left Nav! 15377 * </md-sidenav> 15378 * 15379 * <md-content> 15380 * Center Content 15381 * <md-button ng-click="openLeftMenu()"> 15382 * Open Left Menu 15383 * </md-button> 15384 * </md-content> 15385 * 15386 * <md-sidenav md-component-id="right" 15387 * md-is-locked-open="$mdMedia('min-width: 333px')" 15388 * class="md-sidenav-right"> 15389 * <form> 15390 * <md-input-container> 15391 * <label for="testInput">Test input</label> 15392 * <input id="testInput" type="text" 15393 * ng-model="data" md-autofocus> 15394 * </md-input-container> 15395 * </form> 15396 * </md-sidenav> 15397 * </div> 15398 * </hljs> 15399 * 15400 * <hljs lang="js"> 15401 * var app = angular.module('myApp', ['ngMaterial']); 15402 * app.controller('MyController', function($scope, $mdSidenav) { 15403 * $scope.openLeftMenu = function() { 15404 * $mdSidenav('left').toggle(); 15405 * }; 15406 * }); 15407 * </hljs> 15408 * 15409 * @param {expression=} md-is-open A model bound to whether the sidenav is opened. 15410 * @param {string=} md-component-id componentId to use with $mdSidenav service. 15411 * @param {expression=} md-is-locked-open When this expression evaluates to true, 15412 * the sidenav 'locks open': it falls into the content's flow instead 15413 * of appearing over it. This overrides the `md-is-open` attribute. 15414 * 15415 * The $mdMedia() service is exposed to the is-locked-open attribute, which 15416 * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets. 15417 * Examples: 15418 * 15419 * - `<md-sidenav md-is-locked-open="shouldLockOpen"></md-sidenav>` 15420 * - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>` 15421 * - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens) 15422 */ 15423 function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) { 15424 return { 15425 restrict: 'E', 15426 scope: { 15427 isOpen: '=?mdIsOpen' 15428 }, 15429 controller: '$mdSidenavController', 15430 compile: function(element) { 15431 element.addClass('md-closed'); 15432 element.attr('tabIndex', '-1'); 15433 return postLink; 15434 } 15435 }; 15436 15437 /** 15438 * Directive Post Link function... 15439 */ 15440 function postLink(scope, element, attr, sidenavCtrl) { 15441 var lastParentOverFlow; 15442 var triggeringElement = null; 15443 var promise = $q.when(true); 15444 15445 var isLockedOpenParsed = $parse(attr.mdIsLockedOpen); 15446 var isLocked = function() { 15447 return isLockedOpenParsed(scope.$parent, { 15448 $media: function(arg) { 15449 $log.warn("$media is deprecated for is-locked-open. Use $mdMedia instead."); 15450 return $mdMedia(arg); 15451 }, 15452 $mdMedia: $mdMedia 15453 }); 15454 }; 15455 var backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter"); 15456 15457 $mdTheming(element); 15458 15459 // The backdrop should inherit the sidenavs theme, 15460 // because the backdrop will take its parent theme by default. 15461 $mdTheming.inherit(backdrop, element); 15462 15463 element.on('$destroy', function() { 15464 backdrop.remove(); 15465 sidenavCtrl.destroy(); 15466 }); 15467 15468 scope.$on('$destroy', function(){ 15469 backdrop.remove() 15470 }); 15471 15472 scope.$watch(isLocked, updateIsLocked); 15473 scope.$watch('isOpen', updateIsOpen); 15474 15475 15476 // Publish special accessor for the Controller instance 15477 sidenavCtrl.$toggleOpen = toggleOpen; 15478 15479 /** 15480 * Toggle the DOM classes to indicate `locked` 15481 * @param isLocked 15482 */ 15483 function updateIsLocked(isLocked, oldValue) { 15484 scope.isLockedOpen = isLocked; 15485 if (isLocked === oldValue) { 15486 element.toggleClass('md-locked-open', !!isLocked); 15487 } else { 15488 $animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open'); 15489 } 15490 backdrop.toggleClass('md-locked-open', !!isLocked); 15491 } 15492 15493 /** 15494 * Toggle the SideNav view and attach/detach listeners 15495 * @param isOpen 15496 */ 15497 function updateIsOpen(isOpen) { 15498 // Support deprecated md-sidenav-focus attribute as fallback 15499 var focusEl = $mdUtil.findFocusTarget(element) || $mdUtil.findFocusTarget(element,'[md-sidenav-focus]') || element; 15500 var parent = element.parent(); 15501 15502 parent[isOpen ? 'on' : 'off']('keydown', onKeyDown); 15503 backdrop[isOpen ? 'on' : 'off']('click', close); 15504 15505 if ( isOpen ) { 15506 // Capture upon opening.. 15507 triggeringElement = $document[0].activeElement; 15508 } 15509 15510 disableParentScroll(isOpen); 15511 15512 return promise = $q.all([ 15513 isOpen ? $animate.enter(backdrop, parent) : $animate.leave(backdrop), 15514 $animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed') 15515 ]) 15516 .then(function() { 15517 // Perform focus when animations are ALL done... 15518 if (scope.isOpen) { 15519 focusEl && focusEl.focus(); 15520 } 15521 }); 15522 } 15523 15524 /** 15525 * Prevent parent scrolling (when the SideNav is open) 15526 */ 15527 function disableParentScroll(disabled) { 15528 var parent = element.parent(); 15529 if ( disabled && !lastParentOverFlow ) { 15530 15531 lastParentOverFlow = parent.css('overflow'); 15532 parent.css('overflow', 'hidden'); 15533 15534 } else if (angular.isDefined(lastParentOverFlow)) { 15535 15536 parent.css('overflow', lastParentOverFlow); 15537 lastParentOverFlow = undefined; 15538 15539 } 15540 } 15541 15542 /** 15543 * Toggle the sideNav view and publish a promise to be resolved when 15544 * the view animation finishes. 15545 * 15546 * @param isOpen 15547 * @returns {*} 15548 */ 15549 function toggleOpen( isOpen ) { 15550 if (scope.isOpen == isOpen ) { 15551 15552 return $q.when(true); 15553 15554 } else { 15555 return $q(function(resolve){ 15556 // Toggle value to force an async `updateIsOpen()` to run 15557 scope.isOpen = isOpen; 15558 15559 $mdUtil.nextTick(function() { 15560 // When the current `updateIsOpen()` animation finishes 15561 promise.then(function(result) { 15562 15563 if ( !scope.isOpen ) { 15564 // reset focus to originating element (if available) upon close 15565 triggeringElement && triggeringElement.focus(); 15566 triggeringElement = null; 15567 } 15568 15569 resolve(result); 15570 }); 15571 }); 15572 15573 }); 15574 15575 } 15576 } 15577 15578 /** 15579 * Auto-close sideNav when the `escape` key is pressed. 15580 * @param evt 15581 */ 15582 function onKeyDown(ev) { 15583 var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE); 15584 return isEscape ? close(ev) : $q.when(true); 15585 } 15586 15587 /** 15588 * With backdrop `clicks` or `escape` key-press, immediately 15589 * apply the CSS close transition... Then notify the controller 15590 * to close() and perform its own actions. 15591 */ 15592 function close(ev) { 15593 ev.preventDefault(); 15594 15595 return sidenavCtrl.close(); 15596 } 15597 15598 } 15599 } 15600 SidenavDirective.$inject = ["$mdMedia", "$mdUtil", "$mdConstant", "$mdTheming", "$animate", "$compile", "$parse", "$log", "$q", "$document"]; 15601 15602 /* 15603 * @private 15604 * @ngdoc controller 15605 * @name SidenavController 15606 * @module material.components.sidenav 15607 * 15608 */ 15609 function SidenavController($scope, $element, $attrs, $mdComponentRegistry, $q) { 15610 15611 var self = this; 15612 15613 // Use Default internal method until overridden by directive postLink 15614 15615 // Synchronous getters 15616 self.isOpen = function() { return !!$scope.isOpen; }; 15617 self.isLockedOpen = function() { return !!$scope.isLockedOpen; }; 15618 15619 // Async actions 15620 self.open = function() { return self.$toggleOpen( true ); }; 15621 self.close = function() { return self.$toggleOpen( false ); }; 15622 self.toggle = function() { return self.$toggleOpen( !$scope.isOpen ); }; 15623 self.$toggleOpen = function(value) { return $q.when($scope.isOpen = value); }; 15624 15625 self.destroy = $mdComponentRegistry.register(self, $attrs.mdComponentId); 15626 } 15627 SidenavController.$inject = ["$scope", "$element", "$attrs", "$mdComponentRegistry", "$q"]; 15628 15629 })(); 15630 (function(){ 15631 "use strict"; 15632 15633 /** 15634 * @ngdoc module 15635 * @name material.components.slider 15636 */ 15637 angular.module('material.components.slider', [ 15638 'material.core' 15639 ]) 15640 .directive('mdSlider', SliderDirective); 15641 15642 /** 15643 * @ngdoc directive 15644 * @name mdSlider 15645 * @module material.components.slider 15646 * @restrict E 15647 * @description 15648 * The `<md-slider>` component allows the user to choose from a range of 15649 * values. 15650 * 15651 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) 15652 * the slider is in the accent color by default. The primary color palette may be used with 15653 * the `md-primary` class. 15654 * 15655 * It has two modes: 'normal' mode, where the user slides between a wide range 15656 * of values, and 'discrete' mode, where the user slides between only a few 15657 * select values. 15658 * 15659 * To enable discrete mode, add the `md-discrete` attribute to a slider, 15660 * and use the `step` attribute to change the distance between 15661 * values the user is allowed to pick. 15662 * 15663 * @usage 15664 * <h4>Normal Mode</h4> 15665 * <hljs lang="html"> 15666 * <md-slider ng-model="myValue" min="5" max="500"> 15667 * </md-slider> 15668 * </hljs> 15669 * <h4>Discrete Mode</h4> 15670 * <hljs lang="html"> 15671 * <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130"> 15672 * </md-slider> 15673 * </hljs> 15674 * 15675 * @param {boolean=} md-discrete Whether to enable discrete mode. 15676 * @param {number=} step The distance between values the user is allowed to pick. Default 1. 15677 * @param {number=} min The minimum value the user is allowed to pick. Default 0. 15678 * @param {number=} max The maximum value the user is allowed to pick. Default 100. 15679 */ 15680 function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse, $log) { 15681 return { 15682 scope: {}, 15683 require: '?ngModel', 15684 template: 15685 '<div class="md-slider-wrapper">' + 15686 '<div class="md-track-container">' + 15687 '<div class="md-track"></div>' + 15688 '<div class="md-track md-track-fill"></div>' + 15689 '<div class="md-track-ticks"></div>' + 15690 '</div>' + 15691 '<div class="md-thumb-container">' + 15692 '<div class="md-thumb"></div>' + 15693 '<div class="md-focus-thumb"></div>' + 15694 '<div class="md-focus-ring"></div>' + 15695 '<div class="md-sign">' + 15696 '<span class="md-thumb-text"></span>' + 15697 '</div>' + 15698 '<div class="md-disabled-thumb"></div>' + 15699 '</div>' + 15700 '</div>', 15701 compile: compile 15702 }; 15703 15704 // ********************************************************** 15705 // Private Methods 15706 // ********************************************************** 15707 15708 function compile (tElement, tAttrs) { 15709 if (!tAttrs.tabindex) tElement.attr('tabindex', 0); 15710 tElement.attr('role', 'slider'); 15711 15712 $mdAria.expect(tElement, 'aria-label'); 15713 15714 return postLink; 15715 } 15716 15717 function postLink(scope, element, attr, ngModelCtrl) { 15718 $mdTheming(element); 15719 ngModelCtrl = ngModelCtrl || { 15720 // Mock ngModelController if it doesn't exist to give us 15721 // the minimum functionality needed 15722 $setViewValue: function(val) { 15723 this.$viewValue = val; 15724 this.$viewChangeListeners.forEach(function(cb) { cb(); }); 15725 }, 15726 $parsers: [], 15727 $formatters: [], 15728 $viewChangeListeners: [] 15729 }; 15730 15731 var isDisabled = false; 15732 15733 attr.$observe('disabled', function (value) { 15734 isDisabled = $mdUtil.parseAttributeBoolean(value, false); 15735 updateAriaDisabled(); 15736 }); 15737 15738 var thumb = angular.element(element[0].querySelector('.md-thumb')); 15739 var thumbText = angular.element(element[0].querySelector('.md-thumb-text')); 15740 var thumbContainer = thumb.parent(); 15741 var trackContainer = angular.element(element[0].querySelector('.md-track-container')); 15742 var activeTrack = angular.element(element[0].querySelector('.md-track-fill')); 15743 var tickContainer = angular.element(element[0].querySelector('.md-track-ticks')); 15744 var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000); 15745 15746 // Default values, overridable by attrs 15747 angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0); 15748 angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100); 15749 angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1); 15750 15751 $mdGesture.register(element, 'drag'); 15752 15753 element 15754 .on('keydown', keydownListener) 15755 .on('$md.pressdown', onPressDown) 15756 .on('$md.pressup', onPressUp) 15757 .on('$md.dragstart', onDragStart) 15758 .on('$md.drag', onDrag) 15759 .on('$md.dragend', onDragEnd); 15760 15761 // On resize, recalculate the slider's dimensions and re-render 15762 function updateAll() { 15763 refreshSliderDimensions(); 15764 ngModelRender(); 15765 redrawTicks(); 15766 } 15767 setTimeout(updateAll, 0); 15768 15769 var debouncedUpdateAll = $$rAF.throttle(updateAll); 15770 angular.element($window).on('resize', debouncedUpdateAll); 15771 15772 scope.$on('$destroy', function() { 15773 angular.element($window).off('resize', debouncedUpdateAll); 15774 }); 15775 15776 ngModelCtrl.$render = ngModelRender; 15777 ngModelCtrl.$viewChangeListeners.push(ngModelRender); 15778 ngModelCtrl.$formatters.push(minMaxValidator); 15779 ngModelCtrl.$formatters.push(stepValidator); 15780 15781 /** 15782 * Attributes 15783 */ 15784 var min; 15785 var max; 15786 var step; 15787 function updateMin(value) { 15788 min = parseFloat(value); 15789 element.attr('aria-valuemin', value); 15790 updateAll(); 15791 } 15792 function updateMax(value) { 15793 max = parseFloat(value); 15794 element.attr('aria-valuemax', value); 15795 updateAll(); 15796 } 15797 function updateStep(value) { 15798 step = parseFloat(value); 15799 redrawTicks(); 15800 } 15801 function updateAriaDisabled() { 15802 element.attr('aria-disabled', !!isDisabled); 15803 } 15804 15805 // Draw the ticks with canvas. 15806 // The alternative to drawing ticks with canvas is to draw one element for each tick, 15807 // which could quickly become a performance bottleneck. 15808 var tickCanvas, tickCtx; 15809 function redrawTicks() { 15810 if (!angular.isDefined(attr.mdDiscrete)) return; 15811 if ( angular.isUndefined(step) ) return; 15812 15813 if ( step <= 0 ) { 15814 var msg = 'Slider step value must be greater than zero when in discrete mode'; 15815 $log.error(msg); 15816 throw new Error(msg); 15817 } 15818 15819 var numSteps = Math.floor( (max - min) / step ); 15820 if (!tickCanvas) { 15821 tickCanvas = angular.element('<canvas>').css('position', 'absolute'); 15822 tickContainer.append(tickCanvas); 15823 15824 var trackTicksStyle = $window.getComputedStyle(tickContainer[0]); 15825 tickCtx = tickCanvas[0].getContext('2d'); 15826 tickCtx.fillStyle = trackTicksStyle.backgroundColor || 'black'; 15827 } 15828 15829 var dimensions = getSliderDimensions(); 15830 tickCanvas[0].width = dimensions.width; 15831 tickCanvas[0].height = dimensions.height; 15832 15833 var distance; 15834 for (var i = 0; i <= numSteps; i++) { 15835 distance = Math.floor(dimensions.width * (i / numSteps)); 15836 tickCtx.fillRect(distance - 1, 0, 2, dimensions.height); 15837 } 15838 } 15839 15840 15841 /** 15842 * Refreshing Dimensions 15843 */ 15844 var sliderDimensions = {}; 15845 refreshSliderDimensions(); 15846 function refreshSliderDimensions() { 15847 sliderDimensions = trackContainer[0].getBoundingClientRect(); 15848 } 15849 function getSliderDimensions() { 15850 throttledRefreshDimensions(); 15851 return sliderDimensions; 15852 } 15853 15854 /** 15855 * left/right arrow listener 15856 */ 15857 function keydownListener(ev) { 15858 if (isDisabled) return; 15859 15860 var changeAmount; 15861 if (ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) { 15862 changeAmount = -step; 15863 } else if (ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) { 15864 changeAmount = step; 15865 } 15866 if (changeAmount) { 15867 if (ev.metaKey || ev.ctrlKey || ev.altKey) { 15868 changeAmount *= 4; 15869 } 15870 ev.preventDefault(); 15871 ev.stopPropagation(); 15872 scope.$evalAsync(function() { 15873 setModelValue(ngModelCtrl.$viewValue + changeAmount); 15874 }); 15875 } 15876 } 15877 15878 /** 15879 * ngModel setters and validators 15880 */ 15881 function setModelValue(value) { 15882 ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) ); 15883 } 15884 function ngModelRender() { 15885 if (isNaN(ngModelCtrl.$viewValue)) { 15886 ngModelCtrl.$viewValue = ngModelCtrl.$modelValue; 15887 } 15888 15889 var percent = (ngModelCtrl.$viewValue - min) / (max - min); 15890 scope.modelValue = ngModelCtrl.$viewValue; 15891 element.attr('aria-valuenow', ngModelCtrl.$viewValue); 15892 setSliderPercent(percent); 15893 thumbText.text( ngModelCtrl.$viewValue ); 15894 } 15895 15896 function minMaxValidator(value) { 15897 if (angular.isNumber(value)) { 15898 return Math.max(min, Math.min(max, value)); 15899 } 15900 } 15901 function stepValidator(value) { 15902 if (angular.isNumber(value)) { 15903 var formattedValue = (Math.round((value - min) / step) * step + min); 15904 // Format to 3 digits after the decimal point - fixes #2015. 15905 return (Math.round(formattedValue * 1000) / 1000); 15906 } 15907 } 15908 15909 /** 15910 * @param percent 0-1 15911 */ 15912 function setSliderPercent(percent) { 15913 15914 percent = clamp(percent); 15915 15916 var percentStr = (percent * 100) + '%'; 15917 15918 activeTrack.css('width', percentStr); 15919 thumbContainer.css('left',percentStr); 15920 15921 element.toggleClass('md-min', percent === 0); 15922 element.toggleClass('md-max', percent === 1); 15923 } 15924 15925 15926 /** 15927 * Slide listeners 15928 */ 15929 var isDragging = false; 15930 var isDiscrete = angular.isDefined(attr.mdDiscrete); 15931 15932 function onPressDown(ev) { 15933 if (isDisabled) return; 15934 15935 element.addClass('md-active'); 15936 element[0].focus(); 15937 refreshSliderDimensions(); 15938 15939 var exactVal = percentToValue( positionToPercent( ev.pointer.x )); 15940 var closestVal = minMaxValidator( stepValidator(exactVal) ); 15941 scope.$apply(function() { 15942 setModelValue( closestVal ); 15943 setSliderPercent( valueToPercent(closestVal)); 15944 }); 15945 } 15946 function onPressUp(ev) { 15947 if (isDisabled) return; 15948 15949 element.removeClass('md-dragging md-active'); 15950 15951 var exactVal = percentToValue( positionToPercent( ev.pointer.x )); 15952 var closestVal = minMaxValidator( stepValidator(exactVal) ); 15953 scope.$apply(function() { 15954 setModelValue(closestVal); 15955 ngModelRender(); 15956 }); 15957 } 15958 function onDragStart(ev) { 15959 if (isDisabled) return; 15960 isDragging = true; 15961 ev.stopPropagation(); 15962 15963 element.addClass('md-dragging'); 15964 setSliderFromEvent(ev); 15965 } 15966 function onDrag(ev) { 15967 if (!isDragging) return; 15968 ev.stopPropagation(); 15969 setSliderFromEvent(ev); 15970 } 15971 function onDragEnd(ev) { 15972 if (!isDragging) return; 15973 ev.stopPropagation(); 15974 isDragging = false; 15975 } 15976 15977 function setSliderFromEvent(ev) { 15978 // While panning discrete, update only the 15979 // visual positioning but not the model value. 15980 if ( isDiscrete ) adjustThumbPosition( ev.pointer.x ); 15981 else doSlide( ev.pointer.x ); 15982 } 15983 15984 /** 15985 * Slide the UI by changing the model value 15986 * @param x 15987 */ 15988 function doSlide( x ) { 15989 scope.$evalAsync( function() { 15990 setModelValue( percentToValue( positionToPercent(x) )); 15991 }); 15992 } 15993 15994 /** 15995 * Slide the UI without changing the model (while dragging/panning) 15996 * @param x 15997 */ 15998 function adjustThumbPosition( x ) { 15999 var exactVal = percentToValue( positionToPercent( x )); 16000 var closestVal = minMaxValidator( stepValidator(exactVal) ); 16001 setSliderPercent( positionToPercent(x) ); 16002 thumbText.text( closestVal ); 16003 } 16004 16005 /** 16006 * Clamps the value to be between 0 and 1. 16007 * @param {number} value The value to clamp. 16008 * @returns {number} 16009 */ 16010 function clamp(value) { 16011 return Math.max(0, Math.min(value || 0, 1)); 16012 } 16013 16014 /** 16015 * Convert horizontal position on slider to percentage value of offset from beginning... 16016 * @param x 16017 * @returns {number} 16018 */ 16019 function positionToPercent( x ) { 16020 return Math.max(0, Math.min(1, (x - sliderDimensions.left) / (sliderDimensions.width))); 16021 } 16022 16023 /** 16024 * Convert percentage offset on slide to equivalent model value 16025 * @param percent 16026 * @returns {*} 16027 */ 16028 function percentToValue( percent ) { 16029 return (min + percent * (max - min)); 16030 } 16031 16032 function valueToPercent( val ) { 16033 return (val - min)/(max - min); 16034 } 16035 } 16036 } 16037 SliderDirective.$inject = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse", "$log"]; 16038 16039 })(); 16040 (function(){ 16041 "use strict"; 16042 16043 /** 16044 * @ngdoc module 16045 * @name material.components.sticky 16046 * @description 16047 * Sticky effects for md 16048 * 16049 */ 16050 angular 16051 .module('material.components.sticky', [ 16052 'material.core', 16053 'material.components.content' 16054 ]) 16055 .factory('$mdSticky', MdSticky); 16056 16057 /** 16058 * @ngdoc service 16059 * @name $mdSticky 16060 * @module material.components.sticky 16061 * 16062 * @description 16063 * The `$mdSticky`service provides a mixin to make elements sticky. 16064 * 16065 * By default the `$mdSticky` service compiles the cloned element, when not specified through the `elementClone` 16066 * parameter, in the same scope as the actual element lives. 16067 * 16068 * 16069 * <h3>Notes</h3> 16070 * When using an element which is containing a compiled directive, which changed its DOM structure during compilation, 16071 * you should compile the clone yourself using the plain template.<br/><br/> 16072 * See the right usage below: 16073 * <hljs lang="js"> 16074 * angular.module('myModule') 16075 * .directive('stickySelect', function($mdSticky, $compile) { 16076 * var SELECT_TEMPLATE = 16077 * '<md-select ng-model="selected">' + 16078 * '<md-option>Option 1</md-option>' + 16079 * '</md-select>'; 16080 * 16081 * return { 16082 * restrict: 'E', 16083 * replace: true, 16084 * template: SELECT_TEMPLATE, 16085 * link: function(scope,element) { 16086 * $mdSticky(scope, element, $compile(SELECT_TEMPLATE)(scope)); 16087 * } 16088 * }; 16089 * }); 16090 * </hljs> 16091 * 16092 * @usage 16093 * <hljs lang="js"> 16094 * angular.module('myModule') 16095 * .directive('stickyText', function($mdSticky, $compile) { 16096 * return { 16097 * restrict: 'E', 16098 * template: '<span>Sticky Text</span>', 16099 * link: function(scope,element) { 16100 * $mdSticky(scope, element); 16101 * } 16102 * }; 16103 * }); 16104 * </hljs> 16105 * 16106 * @returns A `$mdSticky` function that takes three arguments: 16107 * - `scope` 16108 * - `element`: The element that will be 'sticky' 16109 * - `elementClone`: A clone of the element, that will be shown 16110 * when the user starts scrolling past the original element. 16111 * If not provided, it will use the result of `element.clone()` and compiles it in the given scope. 16112 */ 16113 function MdSticky($document, $mdConstant, $$rAF, $mdUtil, $compile) { 16114 16115 var browserStickySupport = checkStickySupport(); 16116 16117 /** 16118 * Registers an element as sticky, used internally by directives to register themselves 16119 */ 16120 return function registerStickyElement(scope, element, stickyClone) { 16121 var contentCtrl = element.controller('mdContent'); 16122 if (!contentCtrl) return; 16123 16124 if (browserStickySupport) { 16125 element.css({ 16126 position: browserStickySupport, 16127 top: 0, 16128 'z-index': 2 16129 }); 16130 } else { 16131 var $$sticky = contentCtrl.$element.data('$$sticky'); 16132 if (!$$sticky) { 16133 $$sticky = setupSticky(contentCtrl); 16134 contentCtrl.$element.data('$$sticky', $$sticky); 16135 } 16136 16137 // Compile our cloned element, when cloned in this service, into the given scope. 16138 var cloneElement = stickyClone || $compile(element.clone())(scope); 16139 16140 var deregister = $$sticky.add(element, cloneElement); 16141 scope.$on('$destroy', deregister); 16142 } 16143 }; 16144 16145 function setupSticky(contentCtrl) { 16146 var contentEl = contentCtrl.$element; 16147 16148 // Refresh elements is very expensive, so we use the debounced 16149 // version when possible. 16150 var debouncedRefreshElements = $$rAF.throttle(refreshElements); 16151 16152 // setupAugmentedScrollEvents gives us `$scrollstart` and `$scroll`, 16153 // more reliable than `scroll` on android. 16154 setupAugmentedScrollEvents(contentEl); 16155 contentEl.on('$scrollstart', debouncedRefreshElements); 16156 contentEl.on('$scroll', onScroll); 16157 16158 var self; 16159 return self = { 16160 prev: null, 16161 current: null, //the currently stickied item 16162 next: null, 16163 items: [], 16164 add: add, 16165 refreshElements: refreshElements 16166 }; 16167 16168 /*************** 16169 * Public 16170 ***************/ 16171 // Add an element and its sticky clone to this content's sticky collection 16172 function add(element, stickyClone) { 16173 stickyClone.addClass('md-sticky-clone'); 16174 16175 var item = { 16176 element: element, 16177 clone: stickyClone 16178 }; 16179 self.items.push(item); 16180 16181 $mdUtil.nextTick(function() { 16182 contentEl.prepend(item.clone); 16183 }); 16184 16185 debouncedRefreshElements(); 16186 16187 return function remove() { 16188 self.items.forEach(function(item, index) { 16189 if (item.element[0] === element[0]) { 16190 self.items.splice(index, 1); 16191 item.clone.remove(); 16192 } 16193 }); 16194 debouncedRefreshElements(); 16195 }; 16196 } 16197 16198 function refreshElements() { 16199 // Sort our collection of elements by their current position in the DOM. 16200 // We need to do this because our elements' order of being added may not 16201 // be the same as their order of display. 16202 self.items.forEach(refreshPosition); 16203 self.items = self.items.sort(function(a, b) { 16204 return a.top < b.top ? -1 : 1; 16205 }); 16206 16207 // Find which item in the list should be active, 16208 // based upon the content's current scroll position 16209 var item; 16210 var currentScrollTop = contentEl.prop('scrollTop'); 16211 for (var i = self.items.length - 1; i >= 0; i--) { 16212 if (currentScrollTop > self.items[i].top) { 16213 item = self.items[i]; 16214 break; 16215 } 16216 } 16217 setCurrentItem(item); 16218 } 16219 16220 /*************** 16221 * Private 16222 ***************/ 16223 16224 // Find the `top` of an item relative to the content element, 16225 // and also the height. 16226 function refreshPosition(item) { 16227 // Find the top of an item by adding to the offsetHeight until we reach the 16228 // content element. 16229 var current = item.element[0]; 16230 item.top = 0; 16231 item.left = 0; 16232 while (current && current !== contentEl[0]) { 16233 item.top += current.offsetTop; 16234 item.left += current.offsetLeft; 16235 if ( current.offsetParent ){ 16236 item.right += current.offsetParent.offsetWidth - current.offsetWidth - current.offsetLeft; //Compute offsetRight 16237 } 16238 current = current.offsetParent; 16239 } 16240 item.height = item.element.prop('offsetHeight'); 16241 item.clone.css('margin-left', item.left + 'px'); 16242 if ($mdUtil.floatingScrollbars()) { 16243 item.clone.css('margin-right', '0'); 16244 } 16245 } 16246 16247 // As we scroll, push in and select the correct sticky element. 16248 function onScroll() { 16249 var scrollTop = contentEl.prop('scrollTop'); 16250 var isScrollingDown = scrollTop > (onScroll.prevScrollTop || 0); 16251 16252 // Store the previous scroll so we know which direction we are scrolling 16253 onScroll.prevScrollTop = scrollTop; 16254 16255 // 16256 // AT TOP (not scrolling) 16257 // 16258 if (scrollTop === 0) { 16259 // If we're at the top, just clear the current item and return 16260 setCurrentItem(null); 16261 return; 16262 } 16263 16264 // 16265 // SCROLLING DOWN (going towards the next item) 16266 // 16267 if (isScrollingDown) { 16268 16269 // If we've scrolled down past the next item's position, sticky it and return 16270 if (self.next && self.next.top <= scrollTop) { 16271 setCurrentItem(self.next); 16272 return; 16273 } 16274 16275 // If the next item is close to the current one, push the current one up out of the way 16276 if (self.current && self.next && self.next.top - scrollTop <= self.next.height) { 16277 translate(self.current, scrollTop + (self.next.top - self.next.height - scrollTop)); 16278 return; 16279 } 16280 } 16281 16282 // 16283 // SCROLLING UP (not at the top & not scrolling down; must be scrolling up) 16284 // 16285 if (!isScrollingDown) { 16286 16287 // If we've scrolled up past the previous item's position, sticky it and return 16288 if (self.current && self.prev && scrollTop < self.current.top) { 16289 setCurrentItem(self.prev); 16290 return; 16291 } 16292 16293 // If the next item is close to the current one, pull the current one down into view 16294 if (self.next && self.current && (scrollTop >= (self.next.top - self.current.height))) { 16295 translate(self.current, scrollTop + (self.next.top - scrollTop - self.current.height)); 16296 return; 16297 } 16298 } 16299 16300 // 16301 // Otherwise, just move the current item to the proper place (scrolling up or down) 16302 // 16303 if (self.current) { 16304 translate(self.current, scrollTop); 16305 } 16306 } 16307 16308 function setCurrentItem(item) { 16309 if (self.current === item) return; 16310 // Deactivate currently active item 16311 if (self.current) { 16312 translate(self.current, null); 16313 setStickyState(self.current, null); 16314 } 16315 16316 // Activate new item if given 16317 if (item) { 16318 setStickyState(item, 'active'); 16319 } 16320 16321 self.current = item; 16322 var index = self.items.indexOf(item); 16323 // If index === -1, index + 1 = 0. It works out. 16324 self.next = self.items[index + 1]; 16325 self.prev = self.items[index - 1]; 16326 setStickyState(self.next, 'next'); 16327 setStickyState(self.prev, 'prev'); 16328 } 16329 16330 function setStickyState(item, state) { 16331 if (!item || item.state === state) return; 16332 if (item.state) { 16333 item.clone.attr('sticky-prev-state', item.state); 16334 item.element.attr('sticky-prev-state', item.state); 16335 } 16336 item.clone.attr('sticky-state', state); 16337 item.element.attr('sticky-state', state); 16338 item.state = state; 16339 } 16340 16341 function translate(item, amount) { 16342 if (!item) return; 16343 if (amount === null || amount === undefined) { 16344 if (item.translateY) { 16345 item.translateY = null; 16346 item.clone.css($mdConstant.CSS.TRANSFORM, ''); 16347 } 16348 } else { 16349 item.translateY = amount; 16350 item.clone.css( 16351 $mdConstant.CSS.TRANSFORM, 16352 'translate3d(' + item.left + 'px,' + amount + 'px,0)' 16353 ); 16354 } 16355 } 16356 } 16357 16358 // Function to check for browser sticky support 16359 function checkStickySupport($el) { 16360 var stickyProp; 16361 var testEl = angular.element('<div>'); 16362 $document[0].body.appendChild(testEl[0]); 16363 16364 var stickyProps = ['sticky', '-webkit-sticky']; 16365 for (var i = 0; i < stickyProps.length; ++i) { 16366 testEl.css({position: stickyProps[i], top: 0, 'z-index': 2}); 16367 if (testEl.css('position') == stickyProps[i]) { 16368 stickyProp = stickyProps[i]; 16369 break; 16370 } 16371 } 16372 testEl.remove(); 16373 return stickyProp; 16374 } 16375 16376 // Android 4.4 don't accurately give scroll events. 16377 // To fix this problem, we setup a fake scroll event. We say: 16378 // > If a scroll or touchmove event has happened in the last DELAY milliseconds, 16379 // then send a `$scroll` event every animationFrame. 16380 // Additionally, we add $scrollstart and $scrollend events. 16381 function setupAugmentedScrollEvents(element) { 16382 var SCROLL_END_DELAY = 200; 16383 var isScrolling; 16384 var lastScrollTime; 16385 element.on('scroll touchmove', function() { 16386 if (!isScrolling) { 16387 isScrolling = true; 16388 $$rAF.throttle(loopScrollEvent); 16389 element.triggerHandler('$scrollstart'); 16390 } 16391 element.triggerHandler('$scroll'); 16392 lastScrollTime = +$mdUtil.now(); 16393 }); 16394 16395 function loopScrollEvent() { 16396 if (+$mdUtil.now() - lastScrollTime > SCROLL_END_DELAY) { 16397 isScrolling = false; 16398 element.triggerHandler('$scrollend'); 16399 } else { 16400 element.triggerHandler('$scroll'); 16401 $$rAF.throttle(loopScrollEvent); 16402 } 16403 } 16404 } 16405 16406 } 16407 MdSticky.$inject = ["$document", "$mdConstant", "$$rAF", "$mdUtil", "$compile"]; 16408 16409 })(); 16410 (function(){ 16411 "use strict"; 16412 16413 /** 16414 * @ngdoc module 16415 * @name material.components.subheader 16416 * @description 16417 * SubHeader module 16418 * 16419 * Subheaders are special list tiles that delineate distinct sections of a 16420 * list or grid list and are typically related to the current filtering or 16421 * sorting criteria. Subheader tiles are either displayed inline with tiles or 16422 * can be associated with content, for example, in an adjacent column. 16423 * 16424 * Upon scrolling, subheaders remain pinned to the top of the screen and remain 16425 * pinned until pushed on or off screen by the next subheader. @see [Material 16426 * Design Specifications](https://www.google.com/design/spec/components/subheaders.html) 16427 * 16428 * > To improve the visual grouping of content, use the system color for your subheaders. 16429 * 16430 */ 16431 angular 16432 .module('material.components.subheader', [ 16433 'material.core', 16434 'material.components.sticky' 16435 ]) 16436 .directive('mdSubheader', MdSubheaderDirective); 16437 16438 /** 16439 * @ngdoc directive 16440 * @name mdSubheader 16441 * @module material.components.subheader 16442 * 16443 * @restrict E 16444 * 16445 * @description 16446 * The `<md-subheader>` directive is a subheader for a section. By default it is sticky. 16447 * You can make it not sticky by applying the `md-no-sticky` class to the subheader. 16448 * 16449 * 16450 * @usage 16451 * <hljs lang="html"> 16452 * <md-subheader>Online Friends</md-subheader> 16453 * </hljs> 16454 */ 16455 16456 function MdSubheaderDirective($mdSticky, $compile, $mdTheming, $mdUtil) { 16457 return { 16458 restrict: 'E', 16459 replace: true, 16460 transclude: true, 16461 template: ( 16462 '<div class="md-subheader">' + 16463 ' <div class="md-subheader-inner">' + 16464 ' <span class="md-subheader-content"></span>' + 16465 ' </div>' + 16466 '</div>' 16467 ), 16468 link: function postLink(scope, element, attr, controllers, transclude) { 16469 $mdTheming(element); 16470 var outerHTML = element[0].outerHTML; 16471 16472 function getContent(el) { 16473 return angular.element(el[0].querySelector('.md-subheader-content')); 16474 } 16475 16476 // Transclude the user-given contents of the subheader 16477 // the conventional way. 16478 transclude(scope, function(clone) { 16479 getContent(element).append(clone); 16480 }); 16481 16482 // Create another clone, that uses the outer and inner contents 16483 // of the element, that will be 'stickied' as the user scrolls. 16484 if (!element.hasClass('md-no-sticky')) { 16485 transclude(scope, function(clone) { 16486 // If the user adds an ng-if or ng-repeat directly to the md-subheader element, the 16487 // compiled clone below will only be a comment tag (since they replace their elements with 16488 // a comment) which cannot be properly passed to the $mdSticky; so we wrap it in our own 16489 // DIV to ensure we have something $mdSticky can use 16490 var wrapperHtml = '<div class="md-subheader-wrapper">' + outerHTML + '</div>'; 16491 var stickyClone = $compile(wrapperHtml)(scope); 16492 16493 // Append the sticky 16494 $mdSticky(scope, element, stickyClone); 16495 16496 // Delay initialization until after any `ng-if`/`ng-repeat`/etc has finished before 16497 // attempting to create the clone 16498 $mdUtil.nextTick(function() { 16499 getContent(stickyClone).append(clone); 16500 }); 16501 }); 16502 } 16503 } 16504 } 16505 } 16506 MdSubheaderDirective.$inject = ["$mdSticky", "$compile", "$mdTheming", "$mdUtil"]; 16507 16508 })(); 16509 (function(){ 16510 "use strict"; 16511 16512 /** 16513 * @ngdoc module 16514 * @name material.components.swipe 16515 * @description Swipe module! 16516 */ 16517 /** 16518 * @ngdoc directive 16519 * @module material.components.swipe 16520 * @name mdSwipeLeft 16521 * 16522 * @restrict A 16523 * 16524 * @description 16525 * The md-swipe-left directive allows you to specify custom behavior when an element is swiped 16526 * left. 16527 * 16528 * @usage 16529 * <hljs lang="html"> 16530 * <div md-swipe-left="onSwipeLeft()">Swipe me left!</div> 16531 * </hljs> 16532 */ 16533 /** 16534 * @ngdoc directive 16535 * @module material.components.swipe 16536 * @name mdSwipeRight 16537 * 16538 * @restrict A 16539 * 16540 * @description 16541 * The md-swipe-right directive allows you to specify custom behavior when an element is swiped 16542 * right. 16543 * 16544 * @usage 16545 * <hljs lang="html"> 16546 * <div md-swipe-right="onSwipeRight()">Swipe me right!</div> 16547 * </hljs> 16548 */ 16549 /** 16550 * @ngdoc directive 16551 * @module material.components.swipe 16552 * @name mdSwipeUp 16553 * 16554 * @restrict A 16555 * 16556 * @description 16557 * The md-swipe-up directive allows you to specify custom behavior when an element is swiped 16558 * up. 16559 * 16560 * @usage 16561 * <hljs lang="html"> 16562 * <div md-swipe-up="onSwipeUp()">Swipe me up!</div> 16563 * </hljs> 16564 */ 16565 /** 16566 * @ngdoc directive 16567 * @module material.components.swipe 16568 * @name mdSwipeDown 16569 * 16570 * @restrict A 16571 * 16572 * @description 16573 * The md-swipe-down directive allows you to specify custom behavior when an element is swiped 16574 * down. 16575 * 16576 * @usage 16577 * <hljs lang="html"> 16578 * <div md-swipe-down="onSwipDown()">Swipe me down!</div> 16579 * </hljs> 16580 */ 16581 16582 angular.module('material.components.swipe', ['material.core']) 16583 .directive('mdSwipeLeft', getDirective('SwipeLeft')) 16584 .directive('mdSwipeRight', getDirective('SwipeRight')) 16585 .directive('mdSwipeUp', getDirective('SwipeUp')) 16586 .directive('mdSwipeDown', getDirective('SwipeDown')); 16587 16588 function getDirective(name) { 16589 var directiveName = 'md' + name; 16590 var eventName = '$md.' + name.toLowerCase(); 16591 16592 DirectiveFactory.$inject = ["$parse"]; 16593 return DirectiveFactory; 16594 16595 /* @ngInject */ 16596 function DirectiveFactory($parse) { 16597 return { restrict: 'A', link: postLink }; 16598 function postLink(scope, element, attr) { 16599 var fn = $parse(attr[directiveName]); 16600 element.on(eventName, function(ev) { 16601 scope.$apply(function() { fn(scope, { $event: ev }); }); 16602 }); 16603 } 16604 } 16605 } 16606 16607 16608 16609 })(); 16610 (function(){ 16611 "use strict"; 16612 16613 /** 16614 * @private 16615 * @ngdoc module 16616 * @name material.components.switch 16617 */ 16618 16619 angular.module('material.components.switch', [ 16620 'material.core', 16621 'material.components.checkbox' 16622 ]) 16623 .directive('mdSwitch', MdSwitch); 16624 16625 /** 16626 * @private 16627 * @ngdoc directive 16628 * @module material.components.switch 16629 * @name mdSwitch 16630 * @restrict E 16631 * 16632 * The switch directive is used very much like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D). 16633 * 16634 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) 16635 * the switch is in the accent color by default. The primary color palette may be used with 16636 * the `md-primary` class. 16637 * 16638 * @param {string} ng-model Assignable angular expression to data-bind to. 16639 * @param {string=} name Property name of the form under which the control is published. 16640 * @param {expression=} ng-true-value The value to which the expression should be set when selected. 16641 * @param {expression=} ng-false-value The value to which the expression should be set when not selected. 16642 * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element. 16643 * @param {expression=} ng-disabled En/Disable based on the expression. 16644 * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects. 16645 * @param {string=} aria-label Publish the button label used by screen-readers for accessibility. Defaults to the switch's text. 16646 * 16647 * @usage 16648 * <hljs lang="html"> 16649 * <md-switch ng-model="isActive" aria-label="Finished?"> 16650 * Finished ? 16651 * </md-switch> 16652 * 16653 * <md-switch md-no-ink ng-model="hasInk" aria-label="No Ink Effects"> 16654 * No Ink Effects 16655 * </md-switch> 16656 * 16657 * <md-switch ng-disabled="true" ng-model="isDisabled" aria-label="Disabled"> 16658 * Disabled 16659 * </md-switch> 16660 * 16661 * </hljs> 16662 */ 16663 function MdSwitch(mdCheckboxDirective, $mdUtil, $mdConstant, $parse, $$rAF, $mdGesture) { 16664 var checkboxDirective = mdCheckboxDirective[0]; 16665 16666 return { 16667 restrict: 'E', 16668 priority: 210, // Run before ngAria 16669 transclude: true, 16670 template: 16671 '<div class="md-container">' + 16672 '<div class="md-bar"></div>' + 16673 '<div class="md-thumb-container">' + 16674 '<div class="md-thumb" md-ink-ripple md-ink-ripple-checkbox></div>' + 16675 '</div>'+ 16676 '</div>' + 16677 '<div ng-transclude class="md-label"></div>', 16678 require: '?ngModel', 16679 compile: mdSwitchCompile 16680 }; 16681 16682 function mdSwitchCompile(element, attr) { 16683 var checkboxLink = checkboxDirective.compile(element, attr); 16684 // No transition on initial load. 16685 element.addClass('md-dragging'); 16686 16687 return function (scope, element, attr, ngModel) { 16688 ngModel = ngModel || $mdUtil.fakeNgModel(); 16689 16690 var disabledGetter = null; 16691 if (attr.disabled != null) { 16692 disabledGetter = function() { return true; }; 16693 } else if (attr.ngDisabled) { 16694 disabledGetter = $parse(attr.ngDisabled); 16695 } 16696 16697 var thumbContainer = angular.element(element[0].querySelector('.md-thumb-container')); 16698 var switchContainer = angular.element(element[0].querySelector('.md-container')); 16699 16700 // no transition on initial load 16701 $$rAF(function() { 16702 element.removeClass('md-dragging'); 16703 }); 16704 16705 checkboxLink(scope, element, attr, ngModel); 16706 16707 if (disabledGetter) { 16708 scope.$watch(disabledGetter, function(isDisabled) { 16709 element.attr('tabindex', isDisabled ? -1 : 0); 16710 }); 16711 } 16712 16713 // These events are triggered by setup drag 16714 $mdGesture.register(switchContainer, 'drag'); 16715 switchContainer 16716 .on('$md.dragstart', onDragStart) 16717 .on('$md.drag', onDrag) 16718 .on('$md.dragend', onDragEnd); 16719 16720 var drag; 16721 function onDragStart(ev) { 16722 // Don't go if the switch is disabled. 16723 if (disabledGetter && disabledGetter(scope)) return; 16724 ev.stopPropagation(); 16725 16726 element.addClass('md-dragging'); 16727 drag = {width: thumbContainer.prop('offsetWidth')}; 16728 element.removeClass('transition'); 16729 } 16730 16731 function onDrag(ev) { 16732 if (!drag) return; 16733 ev.stopPropagation(); 16734 ev.srcEvent && ev.srcEvent.preventDefault(); 16735 16736 var percent = ev.pointer.distanceX / drag.width; 16737 16738 //if checked, start from right. else, start from left 16739 var translate = ngModel.$viewValue ? 1 + percent : percent; 16740 // Make sure the switch stays inside its bounds, 0-1% 16741 translate = Math.max(0, Math.min(1, translate)); 16742 16743 thumbContainer.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + (100*translate) + '%,0,0)'); 16744 drag.translate = translate; 16745 } 16746 16747 function onDragEnd(ev) { 16748 if (!drag) return; 16749 ev.stopPropagation(); 16750 16751 element.removeClass('md-dragging'); 16752 thumbContainer.css($mdConstant.CSS.TRANSFORM, ''); 16753 16754 // We changed if there is no distance (this is a click a click), 16755 // or if the drag distance is >50% of the total. 16756 var isChanged = ngModel.$viewValue ? drag.translate > 0.5 : drag.translate < 0.5; 16757 if (isChanged) { 16758 applyModelValue(!ngModel.$viewValue); 16759 } 16760 drag = null; 16761 } 16762 16763 function applyModelValue(newValue) { 16764 scope.$apply(function() { 16765 ngModel.$setViewValue(newValue); 16766 ngModel.$render(); 16767 }); 16768 } 16769 16770 }; 16771 } 16772 16773 16774 } 16775 MdSwitch.$inject = ["mdCheckboxDirective", "$mdUtil", "$mdConstant", "$parse", "$$rAF", "$mdGesture"]; 16776 16777 })(); 16778 (function(){ 16779 "use strict"; 16780 16781 /** 16782 * @ngdoc module 16783 * @name material.components.tabs 16784 * @description 16785 * 16786 * Tabs, created with the `<md-tabs>` directive provide *tabbed* navigation with different styles. 16787 * The Tabs component consists of clickable tabs that are aligned horizontally side-by-side. 16788 * 16789 * Features include support for: 16790 * 16791 * - static or dynamic tabs, 16792 * - responsive designs, 16793 * - accessibility support (ARIA), 16794 * - tab pagination, 16795 * - external or internal tab content, 16796 * - focus indicators and arrow-key navigations, 16797 * - programmatic lookup and access to tab controllers, and 16798 * - dynamic transitions through different tab contents. 16799 * 16800 */ 16801 /* 16802 * @see js folder for tabs implementation 16803 */ 16804 angular.module('material.components.tabs', [ 16805 'material.core', 16806 'material.components.icon' 16807 ]); 16808 16809 })(); 16810 (function(){ 16811 "use strict"; 16812 16813 /** 16814 * @ngdoc module 16815 * @name material.components.toast 16816 * @description 16817 * Toast 16818 */ 16819 angular.module('material.components.toast', [ 16820 'material.core', 16821 'material.components.button' 16822 ]) 16823 .directive('mdToast', MdToastDirective) 16824 .provider('$mdToast', MdToastProvider); 16825 16826 /* @ngInject */ 16827 function MdToastDirective($mdToast) { 16828 return { 16829 restrict: 'E', 16830 link: function postLink(scope, element, attr) { 16831 // When navigation force destroys an interimElement, then 16832 // listen and $destroy() that interim instance... 16833 scope.$on('$destroy', function() { 16834 $mdToast.destroy(); 16835 }); 16836 } 16837 }; 16838 } 16839 MdToastDirective.$inject = ["$mdToast"]; 16840 16841 /** 16842 * @ngdoc service 16843 * @name $mdToast 16844 * @module material.components.toast 16845 * 16846 * @description 16847 * `$mdToast` is a service to build a toast notification on any position 16848 * on the screen with an optional duration, and provides a simple promise API. 16849 * 16850 * The toast will be always positioned at the `bottom`, when the screen size is 16851 * between `600px` and `959px` (`sm` breakpoint) 16852 * 16853 * ## Restrictions on custom toasts 16854 * - The toast's template must have an outer `<md-toast>` element. 16855 * - For a toast action, use element with class `md-action`. 16856 * - Add the class `md-capsule` for curved corners. 16857 * 16858 * ## Parent container notes 16859 * 16860 * The toast is positioned using absolute positioning relative to it's first non-static parent 16861 * container. Thus, if the requested parent container uses static positioning, we will temporarily 16862 * set it's positioning to `relative` while the toast is visible and reset it when the toast is 16863 * hidden. 16864 * 16865 * Because of this, it is usually best to ensure that the parent container has a fixed height and 16866 * prevents scrolling by setting the `overflow: hidden;` style. Since the position is based off of 16867 * the parent's height, the toast may be mispositioned if you allow the parent to scroll. 16868 * 16869 * You can, however, have a scrollable element inside of the container; just make sure the 16870 * container itself does not scroll. 16871 * 16872 * <hljs lang="html"> 16873 * <div layout-fill id="toast-container"> 16874 * <md-content> 16875 * I can have lots of content and scroll! 16876 * </md-content> 16877 * </div> 16878 * </hljs> 16879 * 16880 * @usage 16881 * <hljs lang="html"> 16882 * <div ng-controller="MyController"> 16883 * <md-button ng-click="openToast()"> 16884 * Open a Toast! 16885 * </md-button> 16886 * </div> 16887 * </hljs> 16888 * 16889 * <hljs lang="js"> 16890 * var app = angular.module('app', ['ngMaterial']); 16891 * app.controller('MyController', function($scope, $mdToast) { 16892 * $scope.openToast = function($event) { 16893 * $mdToast.show($mdToast.simple().textContent('Hello!')); 16894 * // Could also do $mdToast.showSimple('Hello'); 16895 * }; 16896 * }); 16897 * </hljs> 16898 */ 16899 16900 /** 16901 * @ngdoc method 16902 * @name $mdToast#showSimple 16903 * 16904 * @param {string} message The message to display inside the toast 16905 * @description 16906 * Convenience method which builds and shows a simple toast. 16907 * 16908 * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or 16909 * rejected with `$mdToast.cancel()`. 16910 * 16911 */ 16912 16913 /** 16914 * @ngdoc method 16915 * @name $mdToast#simple 16916 * 16917 * @description 16918 * Builds a preconfigured toast. 16919 * 16920 * @returns {obj} a `$mdToastPreset` with the following chainable configuration methods. 16921 * 16922 * _**Note:** These configuration methods are provided in addition to the methods provided by 16923 * the `build()` and `show()` methods below._ 16924 * 16925 * - `.textContent(string)` - Sets the toast content to the specified string. 16926 * 16927 * - `.action(string)` - Adds an action button. If clicked, the promise (returned from `show()`) 16928 * will resolve with the value `'ok'`; otherwise, it is resolved with `true` after a `hideDelay` 16929 * timeout. 16930 * 16931 * - `.highlightAction(boolean)` - Whether or not the action button will have an additional 16932 * highlight class. 16933 * 16934 * - `.capsule(boolean)` - Whether or not to add the `md-capsule` class to the toast to provide 16935 * rounded corners. 16936 * 16937 * - `.theme(string)` - Sets the theme on the toast to the requested theme. Default is 16938 * `$mdThemingProvider`'s default. 16939 */ 16940 16941 /** 16942 * @ngdoc method 16943 * @name $mdToast#updateTextContent 16944 * 16945 * @description 16946 * Updates the content of an existing toast. Useful for updating things like counts, etc. 16947 * 16948 */ 16949 16950 /** 16951 * @ngdoc method 16952 * @name $mdToast#build 16953 * 16954 * @description 16955 * Creates a custom `$mdToastPreset` that you can configure. 16956 * 16957 * @returns {obj} a `$mdToastPreset` with the chainable configuration methods for shows' options (see below). 16958 */ 16959 16960 /** 16961 * @ngdoc method 16962 * @name $mdToast#show 16963 * 16964 * @description Shows the toast. 16965 * 16966 * @param {object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()` 16967 * and `build()`, or an options object with the following properties: 16968 * 16969 * - `templateUrl` - `{string=}`: The url of an html template file that will 16970 * be used as the content of the toast. Restrictions: the template must 16971 * have an outer `md-toast` element. 16972 * - `template` - `{string=}`: Same as templateUrl, except this is an actual 16973 * template string. 16974 * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template content with a 16975 * `<div class="md-toast-content">` if one is not provided. Defaults to true. Can be disabled if you provide a 16976 * custom toast directive. 16977 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope. 16978 * This scope will be destroyed when the toast is removed unless `preserveScope` is set to true. 16979 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false 16980 * - `hideDelay` - `{number=}`: How many milliseconds the toast should stay 16981 * active before automatically closing. Set to 0 or false to have the toast stay open until 16982 * closed manually. Default: 3000. 16983 * - `position` - `{string=}`: Where to place the toast. Available: any combination 16984 * of 'bottom', 'left', 'top', 'right'. Default: 'bottom left'. 16985 * - `controller` - `{string=}`: The controller to associate with this toast. 16986 * The controller will be injected the local `$mdToast.hide( )`, which is a function 16987 * used to hide the toast. 16988 * - `locals` - `{string=}`: An object containing key/value pairs. The keys will 16989 * be used as names of values to inject into the controller. For example, 16990 * `locals: {three: 3}` would inject `three` into the controller with the value 16991 * of 3. 16992 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. 16993 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values 16994 * and the toast will not open until the promises resolve. 16995 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. 16996 * - `parent` - `{element=}`: The element to append the toast to. Defaults to appending 16997 * to the root element of the application. 16998 * 16999 * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or 17000 * rejected with `$mdToast.cancel()`. `$mdToast.hide()` will resolve either with a Boolean 17001 * value == 'true' or the value passed as an argument to `$mdToast.hide()`. 17002 * And `$mdToast.cancel()` will resolve the promise with a Boolean value == 'false' 17003 */ 17004 17005 /** 17006 * @ngdoc method 17007 * @name $mdToast#hide 17008 * 17009 * @description 17010 * Hide an existing toast and resolve the promise returned from `$mdToast.show()`. 17011 * 17012 * @param {*=} response An argument for the resolved promise. 17013 * 17014 * @returns {promise} a promise that is called when the existing element is removed from the DOM. 17015 * The promise is resolved with either a Boolean value == 'true' or the value passed as the 17016 * argument to `.hide()`. 17017 * 17018 */ 17019 17020 /** 17021 * @ngdoc method 17022 * @name $mdToast#cancel 17023 * 17024 * @description 17025 * `DEPRECATED` - The promise returned from opening a toast is used only to notify about the closing of the toast. 17026 * As such, there isn't any reason to also allow that promise to be rejected, 17027 * since it's not clear what the difference between resolve and reject would be. 17028 * 17029 * Hide the existing toast and reject the promise returned from 17030 * `$mdToast.show()`. 17031 * 17032 * @param {*=} response An argument for the rejected promise. 17033 * 17034 * @returns {promise} a promise that is called when the existing element is removed from the DOM 17035 * The promise is resolved with a Boolean value == 'false'. 17036 * 17037 */ 17038 17039 function MdToastProvider($$interimElementProvider) { 17040 // Differentiate promise resolves: hide timeout (value == true) and hide action clicks (value == ok). 17041 var ACTION_RESOLVE = 'ok'; 17042 17043 var activeToastContent; 17044 var $mdToast = $$interimElementProvider('$mdToast') 17045 .setDefaults({ 17046 methods: ['position', 'hideDelay', 'capsule', 'parent' ], 17047 options: toastDefaultOptions 17048 }) 17049 .addPreset('simple', { 17050 argOption: 'textContent', 17051 methods: ['textContent', 'content', 'action', 'highlightAction', 'theme', 'parent'], 17052 options: /* @ngInject */ ["$mdToast", "$mdTheming", function($mdToast, $mdTheming) { 17053 var opts = { 17054 template: 17055 '<md-toast md-theme="{{ toast.theme }}" ng-class="{\'md-capsule\': toast.capsule}">' + 17056 ' <div class="md-toast-content">' + 17057 ' <span flex role="alert" aria-relevant="all" aria-atomic="true">' + 17058 ' {{ toast.content }}' + 17059 ' </span>' + 17060 ' <md-button class="md-action" ng-if="toast.action" ng-click="toast.resolve()" ng-class="{\'md-highlight\': toast.highlightAction}">' + 17061 ' {{ toast.action }}' + 17062 ' </md-button>' + 17063 ' </div>' + 17064 '</md-toast>', 17065 controller: /* @ngInject */ ["$scope", function mdToastCtrl($scope) { 17066 var self = this; 17067 $scope.$watch(function() { return activeToastContent; }, function() { 17068 self.content = activeToastContent; 17069 }); 17070 this.resolve = function() { 17071 $mdToast.hide( ACTION_RESOLVE ); 17072 }; 17073 }], 17074 theme: $mdTheming.defaultTheme(), 17075 controllerAs: 'toast', 17076 bindToController: true 17077 }; 17078 return opts; 17079 }] 17080 }) 17081 .addMethod('updateTextContent', updateTextContent) 17082 .addMethod('updateContent', updateTextContent); 17083 17084 function updateTextContent(newContent) { 17085 activeToastContent = newContent; 17086 } 17087 17088 toastDefaultOptions.$inject = ["$animate", "$mdToast", "$mdUtil", "$mdMedia"]; 17089 return $mdToast; 17090 17091 /* @ngInject */ 17092 function toastDefaultOptions($animate, $mdToast, $mdUtil, $mdMedia) { 17093 var SWIPE_EVENTS = '$md.swipeleft $md.swiperight $md.swipeup $md.swipedown'; 17094 return { 17095 onShow: onShow, 17096 onRemove: onRemove, 17097 position: 'bottom left', 17098 themable: true, 17099 hideDelay: 3000, 17100 autoWrap: true, 17101 transformTemplate: function(template, options) { 17102 var shouldAddWrapper = options.autoWrap && template && !/md-toast-content/g.test(template); 17103 17104 if (shouldAddWrapper) { 17105 // Root element of template will be <md-toast>. We need to wrap all of its content inside of 17106 // of <div class="md-toast-content">. All templates provided here should be static, developer-controlled 17107 // content (meaning we're not attempting to guard against XSS). 17108 var templateRoot = document.createElement('md-template'); 17109 templateRoot.innerHTML = template; 17110 17111 for (var i = 0; i < templateRoot.children.length; i++) { 17112 if (templateRoot.children[i].nodeName === 'MD-TOAST') { 17113 var wrapper = angular.element('<div class="md-toast-content">'); 17114 wrapper.append(templateRoot.children[i].children); 17115 templateRoot.children[i].appendChild(wrapper[0]); 17116 } 17117 } 17118 17119 17120 return templateRoot.outerHTML; 17121 } 17122 17123 return template || ''; 17124 } 17125 }; 17126 17127 function onShow(scope, element, options) { 17128 activeToastContent = options.textContent || options.content; // support deprecated #content method 17129 17130 var isSmScreen = !$mdMedia('gt-sm'); 17131 17132 element = $mdUtil.extractElementByName(element, 'md-toast', true); 17133 options.element = element; 17134 17135 options.onSwipe = function(ev, gesture) { 17136 //Add the relevant swipe class to the element so it can animate correctly 17137 var swipe = ev.type.replace('$md.',''); 17138 var direction = swipe.replace('swipe', ''); 17139 17140 // If the swipe direction is down/up but the toast came from top/bottom don't fade away 17141 // Unless the screen is small, then the toast always on bottom 17142 if ((direction === 'down' && options.position.indexOf('top') != -1 && !isSmScreen) || 17143 (direction === 'up' && (options.position.indexOf('bottom') != -1 || isSmScreen))) { 17144 return; 17145 } 17146 17147 if ((direction === 'left' || direction === 'right') && isSmScreen) { 17148 return; 17149 } 17150 17151 element.addClass('md-' + swipe); 17152 $mdUtil.nextTick($mdToast.cancel); 17153 }; 17154 options.openClass = toastOpenClass(options.position); 17155 17156 17157 // 'top left' -> 'md-top md-left' 17158 options.parent.addClass(options.openClass); 17159 17160 // static is the default position 17161 if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) { 17162 options.parent.css('position', 'relative'); 17163 } 17164 17165 element.on(SWIPE_EVENTS, options.onSwipe); 17166 element.addClass(isSmScreen ? 'md-bottom' : options.position.split(' ').map(function(pos) { 17167 return 'md-' + pos; 17168 }).join(' ')); 17169 17170 if (options.parent) options.parent.addClass('md-toast-animating'); 17171 return $animate.enter(element, options.parent).then(function() { 17172 if (options.parent) options.parent.removeClass('md-toast-animating'); 17173 }); 17174 } 17175 17176 function onRemove(scope, element, options) { 17177 element.off(SWIPE_EVENTS, options.onSwipe); 17178 if (options.parent) options.parent.addClass('md-toast-animating'); 17179 if (options.openClass) options.parent.removeClass(options.openClass); 17180 17181 return ((options.$destroy == true) ? element.remove() : $animate.leave(element)) 17182 .then(function () { 17183 if (options.parent) options.parent.removeClass('md-toast-animating'); 17184 if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) { 17185 options.parent.css('position', ''); 17186 } 17187 }); 17188 } 17189 17190 function toastOpenClass(position) { 17191 if (!$mdMedia('gt-sm')) { 17192 return 'md-toast-open-bottom'; 17193 } 17194 17195 return 'md-toast-open-' + 17196 (position.indexOf('top') > -1 ? 'top' : 'bottom'); 17197 } 17198 } 17199 17200 } 17201 MdToastProvider.$inject = ["$$interimElementProvider"]; 17202 17203 })(); 17204 (function(){ 17205 "use strict"; 17206 17207 /** 17208 * @ngdoc module 17209 * @name material.components.toolbar 17210 */ 17211 angular.module('material.components.toolbar', [ 17212 'material.core', 17213 'material.components.content' 17214 ]) 17215 .directive('mdToolbar', mdToolbarDirective); 17216 17217 /** 17218 * @ngdoc directive 17219 * @name mdToolbar 17220 * @module material.components.toolbar 17221 * @restrict E 17222 * @description 17223 * `md-toolbar` is used to place a toolbar in your app. 17224 * 17225 * Toolbars are usually used above a content area to display the title of the 17226 * current page, and show relevant action buttons for that page. 17227 * 17228 * You can change the height of the toolbar by adding either the 17229 * `md-medium-tall` or `md-tall` class to the toolbar. 17230 * 17231 * @usage 17232 * <hljs lang="html"> 17233 * <div layout="column" layout-fill> 17234 * <md-toolbar> 17235 * 17236 * <div class="md-toolbar-tools"> 17237 * <span>My App's Title</span> 17238 * 17239 * <!-- fill up the space between left and right area --> 17240 * <span flex></span> 17241 * 17242 * <md-button> 17243 * Right Bar Button 17244 * </md-button> 17245 * </div> 17246 * 17247 * </md-toolbar> 17248 * <md-content> 17249 * Hello! 17250 * </md-content> 17251 * </div> 17252 * </hljs> 17253 * 17254 * @param {boolean=} md-scroll-shrink Whether the header should shrink away as 17255 * the user scrolls down, and reveal itself as the user scrolls up. 17256 * 17257 * _**Note (1):** for scrollShrink to work, the toolbar must be a sibling of a 17258 * `md-content` element, placed before it. See the scroll shrink demo._ 17259 * 17260 * _**Note (2):** The `md-scroll-shrink` attribute is only parsed on component 17261 * initialization, it does not watch for scope changes._ 17262 * 17263 * 17264 * @param {number=} md-shrink-speed-factor How much to change the speed of the toolbar's 17265 * shrinking by. For example, if 0.25 is given then the toolbar will shrink 17266 * at one fourth the rate at which the user scrolls down. Default 0.5. 17267 */ 17268 17269 function mdToolbarDirective($$rAF, $mdConstant, $mdUtil, $mdTheming, $animate) { 17270 var translateY = angular.bind(null, $mdUtil.supplant, 'translate3d(0,{0}px,0)'); 17271 17272 return { 17273 template: '', 17274 17275 restrict: 'E', 17276 17277 link: function(scope, element, attr) { 17278 17279 $mdTheming(element); 17280 17281 if (angular.isDefined(attr.mdScrollShrink)) { 17282 setupScrollShrink(); 17283 } 17284 17285 function setupScrollShrink() { 17286 17287 var toolbarHeight; 17288 var contentElement; 17289 var disableScrollShrink = angular.noop; 17290 17291 // Current "y" position of scroll 17292 // Store the last scroll top position 17293 var y = 0; 17294 var prevScrollTop = 0; 17295 var shrinkSpeedFactor = attr.mdShrinkSpeedFactor || 0.5; 17296 17297 var debouncedContentScroll = $$rAF.throttle(onContentScroll); 17298 var debouncedUpdateHeight = $mdUtil.debounce(updateToolbarHeight, 5 * 1000); 17299 17300 // Wait for $mdContentLoaded event from mdContent directive. 17301 // If the mdContent element is a sibling of our toolbar, hook it up 17302 // to scroll events. 17303 17304 scope.$on('$mdContentLoaded', onMdContentLoad); 17305 17306 // If the toolbar is used inside an ng-if statement, we may miss the 17307 // $mdContentLoaded event, so we attempt to fake it if we have a 17308 // md-content close enough. 17309 17310 attr.$observe('mdScrollShrink', onChangeScrollShrink); 17311 17312 // If the toolbar has ngShow or ngHide we need to update height immediately as it changed 17313 // and not wait for $mdUtil.debounce to happen 17314 17315 if (attr.ngShow) { scope.$watch(attr.ngShow, updateToolbarHeight); } 17316 if (attr.ngHide) { scope.$watch(attr.ngHide, updateToolbarHeight); } 17317 17318 // If the scope is destroyed (which could happen with ng-if), make sure 17319 // to disable scroll shrinking again 17320 17321 scope.$on('$destroy', disableScrollShrink); 17322 17323 /** 17324 * 17325 */ 17326 function onChangeScrollShrink(shrinkWithScroll) { 17327 var closestContent = element.parent().find('md-content'); 17328 17329 // If we have a content element, fake the call; this might still fail 17330 // if the content element isn't a sibling of the toolbar 17331 17332 if (!contentElement && closestContent.length) { 17333 onMdContentLoad(null, closestContent); 17334 } 17335 17336 // Evaluate the expression 17337 shrinkWithScroll = scope.$eval(shrinkWithScroll); 17338 17339 // Disable only if the attribute's expression evaluates to false 17340 if (shrinkWithScroll === false) { 17341 disableScrollShrink(); 17342 } else { 17343 disableScrollShrink = enableScrollShrink(); 17344 } 17345 } 17346 17347 /** 17348 * 17349 */ 17350 function onMdContentLoad($event, newContentEl) { 17351 // Toolbar and content must be siblings 17352 if (newContentEl && element.parent()[0] === newContentEl.parent()[0]) { 17353 // unhook old content event listener if exists 17354 if (contentElement) { 17355 contentElement.off('scroll', debouncedContentScroll); 17356 } 17357 17358 contentElement = newContentEl; 17359 disableScrollShrink = enableScrollShrink(); 17360 } 17361 } 17362 17363 /** 17364 * 17365 */ 17366 function onContentScroll(e) { 17367 var scrollTop = e ? e.target.scrollTop : prevScrollTop; 17368 17369 debouncedUpdateHeight(); 17370 17371 y = Math.min( 17372 toolbarHeight / shrinkSpeedFactor, 17373 Math.max(0, y + scrollTop - prevScrollTop) 17374 ); 17375 17376 element.css($mdConstant.CSS.TRANSFORM, translateY([-y * shrinkSpeedFactor])); 17377 contentElement.css($mdConstant.CSS.TRANSFORM, translateY([(toolbarHeight - y) * shrinkSpeedFactor])); 17378 17379 prevScrollTop = scrollTop; 17380 17381 $mdUtil.nextTick(function() { 17382 var hasWhiteFrame = element.hasClass('md-whiteframe-z1'); 17383 17384 if (hasWhiteFrame && !y) { 17385 $animate.removeClass(element, 'md-whiteframe-z1'); 17386 } else if (!hasWhiteFrame && y) { 17387 $animate.addClass(element, 'md-whiteframe-z1'); 17388 } 17389 }); 17390 17391 } 17392 17393 /** 17394 * 17395 */ 17396 function enableScrollShrink() { 17397 if (!contentElement) return angular.noop; // no md-content 17398 17399 contentElement.on('scroll', debouncedContentScroll); 17400 contentElement.attr('scroll-shrink', 'true'); 17401 17402 $$rAF(updateToolbarHeight); 17403 17404 return function disableScrollShrink() { 17405 contentElement.off('scroll', debouncedContentScroll); 17406 contentElement.attr('scroll-shrink', 'false'); 17407 17408 $$rAF(updateToolbarHeight); 17409 } 17410 } 17411 17412 /** 17413 * 17414 */ 17415 function updateToolbarHeight() { 17416 toolbarHeight = element.prop('offsetHeight'); 17417 // Add a negative margin-top the size of the toolbar to the content el. 17418 // The content will start transformed down the toolbarHeight amount, 17419 // so everything looks normal. 17420 // 17421 // As the user scrolls down, the content will be transformed up slowly 17422 // to put the content underneath where the toolbar was. 17423 var margin = (-toolbarHeight * shrinkSpeedFactor) + 'px'; 17424 17425 contentElement.css({ 17426 "margin-top": margin, 17427 "margin-bottom": margin 17428 }); 17429 17430 onContentScroll(); 17431 } 17432 17433 } 17434 17435 } 17436 }; 17437 17438 } 17439 mdToolbarDirective.$inject = ["$$rAF", "$mdConstant", "$mdUtil", "$mdTheming", "$animate"]; 17440 17441 })(); 17442 (function(){ 17443 "use strict"; 17444 17445 /** 17446 * @ngdoc module 17447 * @name material.components.tooltip 17448 */ 17449 angular 17450 .module('material.components.tooltip', [ 'material.core' ]) 17451 .directive('mdTooltip', MdTooltipDirective); 17452 17453 /** 17454 * @ngdoc directive 17455 * @name mdTooltip 17456 * @module material.components.tooltip 17457 * @description 17458 * Tooltips are used to describe elements that are interactive and primarily graphical (not textual). 17459 * 17460 * Place a `<md-tooltip>` as a child of the element it describes. 17461 * 17462 * A tooltip will activate when the user focuses, hovers over, or touches the parent. 17463 * 17464 * @usage 17465 * <hljs lang="html"> 17466 * <md-button class="md-fab md-accent" aria-label="Play"> 17467 * <md-tooltip> 17468 * Play Music 17469 * </md-tooltip> 17470 * <md-icon icon="img/icons/ic_play_arrow_24px.svg"></md-icon> 17471 * </md-button> 17472 * </hljs> 17473 * 17474 * @param {expression=} md-visible Boolean bound to whether the tooltip is currently visible. 17475 * @param {number=} md-delay How many milliseconds to wait to show the tooltip after the user focuses, hovers, or touches the parent. Defaults to 300ms. 17476 * @param {boolean=} md-autohide If present or provided with a boolean value, the tooltip will hide on mouse leave, regardless of focus 17477 * @param {string=} md-direction Which direction would you like the tooltip to go? Supports left, right, top, and bottom. Defaults to bottom. 17478 */ 17479 function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdTheming, $rootElement, 17480 $animate, $q) { 17481 17482 var TOOLTIP_SHOW_DELAY = 0; 17483 var TOOLTIP_WINDOW_EDGE_SPACE = 8; 17484 17485 return { 17486 restrict: 'E', 17487 transclude: true, 17488 priority:210, // Before ngAria 17489 template: '<div class="md-content" ng-transclude></div>', 17490 scope: { 17491 delay: '=?mdDelay', 17492 visible: '=?mdVisible', 17493 autohide: '=?mdAutohide', 17494 direction: '@?mdDirection' // only expect raw or interpolated string value; not expression 17495 }, 17496 link: postLink 17497 }; 17498 17499 function postLink(scope, element, attr) { 17500 17501 $mdTheming(element); 17502 17503 var parent = $mdUtil.getParentWithPointerEvents(element), 17504 content = angular.element(element[0].getElementsByClassName('md-content')[0]), 17505 tooltipParent = angular.element(document.body), 17506 debouncedOnResize = $$rAF.throttle(function () { updatePosition(); }); 17507 17508 if ($animate.pin) $animate.pin(element, parent); 17509 17510 // Initialize element 17511 17512 setDefaults(); 17513 manipulateElement(); 17514 bindEvents(); 17515 17516 // Default origin transform point is 'center top' 17517 // positionTooltip() is always relative to center top 17518 updateContentOrigin(); 17519 17520 configureWatchers(); 17521 addAriaLabel(); 17522 17523 17524 function setDefaults () { 17525 if (!angular.isDefined(attr.mdDelay)) scope.delay = TOOLTIP_SHOW_DELAY; 17526 } 17527 17528 function updateContentOrigin() { 17529 var origin = 'center top'; 17530 switch (scope.direction) { 17531 case 'left' : origin = 'right center'; break; 17532 case 'right' : origin = 'left center'; break; 17533 case 'top' : origin = 'center bottom'; break; 17534 case 'bottom': origin = 'center top'; break; 17535 } 17536 content.css('transform-origin', origin); 17537 } 17538 17539 function configureWatchers () { 17540 scope.$on('$destroy', function() { 17541 scope.visible = false; 17542 element.remove(); 17543 angular.element($window).off('resize', debouncedOnResize); 17544 }); 17545 17546 scope.$watch('visible', function (isVisible) { 17547 if (isVisible) showTooltip(); 17548 else hideTooltip(); 17549 }); 17550 17551 scope.$watch('direction', updatePosition ); 17552 } 17553 17554 function addAriaLabel () { 17555 if (!parent.attr('aria-label') && !parent.text().trim()) { 17556 parent.attr('aria-label', element.text().trim()); 17557 } 17558 } 17559 17560 function manipulateElement () { 17561 element.detach(); 17562 element.attr('role', 'tooltip'); 17563 } 17564 17565 function bindEvents () { 17566 var mouseActive = false; 17567 17568 var ngWindow = angular.element($window); 17569 17570 // add an mutationObserver when there is support for it 17571 // and the need for it in the form of viable host(parent[0]) 17572 if (parent[0] && 'MutationObserver' in $window) { 17573 // use an mutationObserver to tackle #2602 17574 var attributeObserver = new MutationObserver(function(mutations) { 17575 mutations 17576 .forEach(function (mutation) { 17577 if (mutation.attributeName === 'disabled' && parent[0].disabled) { 17578 setVisible(false); 17579 scope.$digest(); // make sure the elements gets updated 17580 } 17581 }); 17582 }); 17583 17584 attributeObserver.observe(parent[0], { attributes: true}); 17585 } 17586 17587 // Store whether the element was focused when the window loses focus. 17588 var windowBlurHandler = function() { 17589 elementFocusedOnWindowBlur = document.activeElement === parent[0]; 17590 }; 17591 var elementFocusedOnWindowBlur = false; 17592 17593 function windowScrollHandler() { 17594 setVisible(false); 17595 } 17596 17597 ngWindow.on('blur', windowBlurHandler); 17598 ngWindow.on('resize', debouncedOnResize); 17599 document.addEventListener('scroll', windowScrollHandler, true); 17600 scope.$on('$destroy', function() { 17601 ngWindow.off('blur', windowBlurHandler); 17602 ngWindow.off('resize', debouncedOnResize); 17603 document.removeEventListener('scroll', windowScrollHandler, true); 17604 attributeObserver && attributeObserver.disconnect(); 17605 }); 17606 17607 var enterHandler = function(e) { 17608 // Prevent the tooltip from showing when the window is receiving focus. 17609 if (e.type === 'focus' && elementFocusedOnWindowBlur) { 17610 elementFocusedOnWindowBlur = false; 17611 return; 17612 } 17613 parent.on('blur mouseleave touchend touchcancel', leaveHandler ); 17614 setVisible(true); 17615 }; 17616 var leaveHandler = function () { 17617 var autohide = scope.hasOwnProperty('autohide') ? scope.autohide : attr.hasOwnProperty('mdAutohide'); 17618 if (autohide || mouseActive || ($document[0].activeElement !== parent[0]) ) { 17619 parent.off('blur mouseleave touchend touchcancel', leaveHandler ); 17620 parent.triggerHandler("blur"); 17621 setVisible(false); 17622 } 17623 mouseActive = false; 17624 }; 17625 17626 // to avoid `synthetic clicks` we listen to mousedown instead of `click` 17627 parent.on('mousedown', function() { mouseActive = true; }); 17628 parent.on('focus mouseenter touchstart', enterHandler ); 17629 17630 17631 } 17632 17633 function setVisible (value) { 17634 setVisible.value = !!value; 17635 if (!setVisible.queued) { 17636 if (value) { 17637 setVisible.queued = true; 17638 $timeout(function() { 17639 scope.visible = setVisible.value; 17640 setVisible.queued = false; 17641 }, scope.delay); 17642 } else { 17643 $mdUtil.nextTick(function() { scope.visible = false; }); 17644 } 17645 } 17646 } 17647 17648 function showTooltip() { 17649 // Insert the element before positioning it, so we can get the position 17650 // and check if we should display it 17651 tooltipParent.append(element); 17652 17653 // Check if we should display it or not. 17654 // This handles hide-* and show-* along with any user defined css 17655 if ( $mdUtil.hasComputedStyle(element, 'display', 'none')) { 17656 scope.visible = false; 17657 element.detach(); 17658 return; 17659 } 17660 17661 updatePosition(); 17662 17663 angular.forEach([element, content], function (element) { 17664 $animate.addClass(element, 'md-show'); 17665 }); 17666 } 17667 17668 function hideTooltip() { 17669 var promises = []; 17670 angular.forEach([element, content], function (it) { 17671 if (it.parent() && it.hasClass('md-show')) { 17672 promises.push($animate.removeClass(it, 'md-show')); 17673 } 17674 }); 17675 17676 $q.all(promises) 17677 .then(function () { 17678 if (!scope.visible) element.detach(); 17679 }); 17680 } 17681 17682 function updatePosition() { 17683 if ( !scope.visible ) return; 17684 17685 updateContentOrigin(); 17686 positionTooltip(); 17687 } 17688 17689 function positionTooltip() { 17690 var tipRect = $mdUtil.offsetRect(element, tooltipParent); 17691 var parentRect = $mdUtil.offsetRect(parent, tooltipParent); 17692 var newPosition = getPosition(scope.direction); 17693 var offsetParent = element.prop('offsetParent'); 17694 17695 // If the user provided a direction, just nudge the tooltip onto the screen 17696 // Otherwise, recalculate based on 'top' since default is 'bottom' 17697 if (scope.direction) { 17698 newPosition = fitInParent(newPosition); 17699 } else if (offsetParent && newPosition.top > offsetParent.scrollHeight - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE) { 17700 newPosition = fitInParent(getPosition('top')); 17701 } 17702 17703 element.css({ 17704 left: newPosition.left + 'px', 17705 top: newPosition.top + 'px' 17706 }); 17707 17708 function fitInParent (pos) { 17709 var newPosition = { left: pos.left, top: pos.top }; 17710 newPosition.left = Math.min( newPosition.left, tooltipParent.prop('scrollWidth') - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE ); 17711 newPosition.left = Math.max( newPosition.left, TOOLTIP_WINDOW_EDGE_SPACE ); 17712 newPosition.top = Math.min( newPosition.top, tooltipParent.prop('scrollHeight') - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE ); 17713 newPosition.top = Math.max( newPosition.top, TOOLTIP_WINDOW_EDGE_SPACE ); 17714 return newPosition; 17715 } 17716 17717 function getPosition (dir) { 17718 return dir === 'left' 17719 ? { left: parentRect.left - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE, 17720 top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 } 17721 : dir === 'right' 17722 ? { left: parentRect.left + parentRect.width + TOOLTIP_WINDOW_EDGE_SPACE, 17723 top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 } 17724 : dir === 'top' 17725 ? { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2, 17726 top: parentRect.top - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE } 17727 : { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2, 17728 top: parentRect.top + parentRect.height + TOOLTIP_WINDOW_EDGE_SPACE }; 17729 } 17730 } 17731 17732 } 17733 17734 } 17735 MdTooltipDirective.$inject = ["$timeout", "$window", "$$rAF", "$document", "$mdUtil", "$mdTheming", "$rootElement", "$animate", "$q"]; 17736 17737 })(); 17738 (function(){ 17739 "use strict"; 17740 17741 /** 17742 * @ngdoc module 17743 * @name material.components.virtualRepeat 17744 */ 17745 angular.module('material.components.virtualRepeat', [ 17746 'material.core', 17747 'material.components.showHide' 17748 ]) 17749 .directive('mdVirtualRepeatContainer', VirtualRepeatContainerDirective) 17750 .directive('mdVirtualRepeat', VirtualRepeatDirective); 17751 17752 17753 /** 17754 * @ngdoc directive 17755 * @name mdVirtualRepeatContainer 17756 * @module material.components.virtualRepeat 17757 * @restrict E 17758 * @description 17759 * `md-virtual-repeat-container` provides the scroll container for md-virtual-repeat. 17760 * 17761 * Virtual repeat is a limited substitute for ng-repeat that renders only 17762 * enough dom nodes to fill the container and recycling them as the user scrolls. 17763 * 17764 * @usage 17765 * <hljs lang="html"> 17766 * 17767 * <md-virtual-repeat-container md-top-index="topIndex"> 17768 * <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div> 17769 * </md-virtual-repeat-container> 17770 * </hljs> 17771 * 17772 * @param {number=} md-top-index Binds the index of the item that is at the top of the scroll 17773 * container to $scope. It can both read and set the scroll position. 17774 * @param {boolean=} md-orient-horizontal Whether the container should scroll horizontally 17775 * (defaults to orientation and scrolling vertically). 17776 * @param {boolean=} md-auto-shrink When present, the container will shrink to fit 17777 * the number of items when that number is less than its original size. 17778 * @param {number=} md-auto-shrink-min Minimum number of items that md-auto-shrink 17779 * will shrink to (default: 0). 17780 */ 17781 function VirtualRepeatContainerDirective() { 17782 return { 17783 controller: VirtualRepeatContainerController, 17784 template: virtualRepeatContainerTemplate, 17785 compile: function virtualRepeatContainerCompile($element, $attrs) { 17786 $element 17787 .addClass('md-virtual-repeat-container') 17788 .addClass($attrs.hasOwnProperty('mdOrientHorizontal') 17789 ? 'md-orient-horizontal' 17790 : 'md-orient-vertical'); 17791 } 17792 }; 17793 } 17794 17795 17796 function virtualRepeatContainerTemplate($element) { 17797 return '<div class="md-virtual-repeat-scroller">' + 17798 '<div class="md-virtual-repeat-sizer"></div>' + 17799 '<div class="md-virtual-repeat-offsetter">' + 17800 $element[0].innerHTML + 17801 '</div></div>'; 17802 } 17803 17804 /** 17805 * Maximum size, in pixels, that can be explicitly set to an element. The actual value varies 17806 * between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could 17807 * *compute* this value, but Firefox always reports an element to have a size of zero if it 17808 * goes over the max, meaning that we'd have to binary search for the value. 17809 * @const {number} 17810 */ 17811 var MAX_ELEMENT_SIZE = 1533917; 17812 17813 /** 17814 * Number of additional elements to render above and below the visible area inside 17815 * of the virtual repeat container. A higher number results in less flicker when scrolling 17816 * very quickly in Safari, but comes with a higher rendering and dirty-checking cost. 17817 * @const {number} 17818 */ 17819 var NUM_EXTRA = 3; 17820 17821 /** @ngInject */ 17822 function VirtualRepeatContainerController( 17823 $$rAF, $mdUtil, $parse, $rootScope, $window, $scope, $element, $attrs) { 17824 this.$rootScope = $rootScope; 17825 this.$scope = $scope; 17826 this.$element = $element; 17827 this.$attrs = $attrs; 17828 17829 /** @type {number} The width or height of the container */ 17830 this.size = 0; 17831 /** @type {number} The scroll width or height of the scroller */ 17832 this.scrollSize = 0; 17833 /** @type {number} The scrollLeft or scrollTop of the scroller */ 17834 this.scrollOffset = 0; 17835 /** @type {boolean} Whether the scroller is oriented horizontally */ 17836 this.horizontal = this.$attrs.hasOwnProperty('mdOrientHorizontal'); 17837 /** @type {!VirtualRepeatController} The repeater inside of this container */ 17838 this.repeater = null; 17839 /** @type {boolean} Whether auto-shrink is enabled */ 17840 this.autoShrink = this.$attrs.hasOwnProperty('mdAutoShrink'); 17841 /** @type {number} Minimum number of items to auto-shrink to */ 17842 this.autoShrinkMin = parseInt(this.$attrs.mdAutoShrinkMin, 10) || 0; 17843 /** @type {?number} Original container size when shrank */ 17844 this.originalSize = null; 17845 /** @type {number} Amount to offset the total scroll size by. */ 17846 this.offsetSize = parseInt(this.$attrs.mdOffsetSize, 10) || 0; 17847 /** @type {?string} height or width element style on the container prior to auto-shrinking. */ 17848 this.oldElementSize = null; 17849 17850 if (this.$attrs.mdTopIndex) { 17851 /** @type {function(angular.Scope): number} Binds to topIndex on Angular scope */ 17852 this.bindTopIndex = $parse(this.$attrs.mdTopIndex); 17853 /** @type {number} The index of the item that is at the top of the scroll container */ 17854 this.topIndex = this.bindTopIndex(this.$scope); 17855 17856 if (!angular.isDefined(this.topIndex)) { 17857 this.topIndex = 0; 17858 this.bindTopIndex.assign(this.$scope, 0); 17859 } 17860 17861 this.$scope.$watch(this.bindTopIndex, angular.bind(this, function(newIndex) { 17862 if (newIndex !== this.topIndex) { 17863 this.scrollToIndex(newIndex); 17864 } 17865 })); 17866 } else { 17867 this.topIndex = 0; 17868 } 17869 17870 this.scroller = $element[0].getElementsByClassName('md-virtual-repeat-scroller')[0]; 17871 this.sizer = this.scroller.getElementsByClassName('md-virtual-repeat-sizer')[0]; 17872 this.offsetter = this.scroller.getElementsByClassName('md-virtual-repeat-offsetter')[0]; 17873 17874 // After the dom stablizes, measure the initial size of the container and 17875 // make a best effort at re-measuring as it changes. 17876 var boundUpdateSize = angular.bind(this, this.updateSize); 17877 17878 $$rAF(angular.bind(this, function() { 17879 boundUpdateSize(); 17880 17881 var debouncedUpdateSize = $mdUtil.debounce(boundUpdateSize, 10, null, false); 17882 var jWindow = angular.element($window); 17883 17884 // Make one more attempt to get the size if it is 0. 17885 // This is not by any means a perfect approach, but there's really no 17886 // silver bullet here. 17887 if (!this.size) { 17888 debouncedUpdateSize(); 17889 } 17890 17891 jWindow.on('resize', debouncedUpdateSize); 17892 $scope.$on('$destroy', function() { 17893 jWindow.off('resize', debouncedUpdateSize); 17894 }); 17895 17896 $scope.$emit('$md-resize-enable'); 17897 $scope.$on('$md-resize', boundUpdateSize); 17898 })); 17899 } 17900 VirtualRepeatContainerController.$inject = ["$$rAF", "$mdUtil", "$parse", "$rootScope", "$window", "$scope", "$element", "$attrs"]; 17901 17902 17903 /** Called by the md-virtual-repeat inside of the container at startup. */ 17904 VirtualRepeatContainerController.prototype.register = function(repeaterCtrl) { 17905 this.repeater = repeaterCtrl; 17906 17907 angular.element(this.scroller) 17908 .on('scroll wheel touchmove touchend', angular.bind(this, this.handleScroll_)); 17909 }; 17910 17911 17912 /** @return {boolean} Whether the container is configured for horizontal scrolling. */ 17913 VirtualRepeatContainerController.prototype.isHorizontal = function() { 17914 return this.horizontal; 17915 }; 17916 17917 17918 /** @return {number} The size (width or height) of the container. */ 17919 VirtualRepeatContainerController.prototype.getSize = function() { 17920 return this.size; 17921 }; 17922 17923 17924 /** 17925 * Resizes the container. 17926 * @private 17927 * @param {number} The new size to set. 17928 */ 17929 VirtualRepeatContainerController.prototype.setSize_ = function(size) { 17930 var dimension = this.getDimensionName_(); 17931 17932 this.size = size; 17933 this.$element[0].style[dimension] = size + 'px'; 17934 }; 17935 17936 17937 VirtualRepeatContainerController.prototype.unsetSize_ = function() { 17938 this.$element[0].style[this.getDimensionName_()] = this.oldElementSize; 17939 this.oldElementSize = null; 17940 }; 17941 17942 17943 /** Instructs the container to re-measure its size. */ 17944 VirtualRepeatContainerController.prototype.updateSize = function() { 17945 if (this.originalSize) return; 17946 17947 this.size = this.isHorizontal() 17948 ? this.$element[0].clientWidth 17949 : this.$element[0].clientHeight; 17950 17951 // Recheck the scroll position after updating the size. This resolves 17952 // problems that can result if the scroll position was measured while the 17953 // element was display: none or detached from the document. 17954 this.handleScroll_(); 17955 17956 this.repeater && this.repeater.containerUpdated(); 17957 }; 17958 17959 17960 /** @return {number} The container's scrollHeight or scrollWidth. */ 17961 VirtualRepeatContainerController.prototype.getScrollSize = function() { 17962 return this.scrollSize; 17963 }; 17964 17965 17966 VirtualRepeatContainerController.prototype.getDimensionName_ = function() { 17967 return this.isHorizontal() ? 'width' : 'height'; 17968 }; 17969 17970 17971 /** 17972 * Sets the scroller element to the specified size. 17973 * @private 17974 * @param {number} size The new size. 17975 */ 17976 VirtualRepeatContainerController.prototype.sizeScroller_ = function(size) { 17977 var dimension = this.getDimensionName_(); 17978 var crossDimension = this.isHorizontal() ? 'height' : 'width'; 17979 17980 // Clear any existing dimensions. 17981 this.sizer.innerHTML = ''; 17982 17983 // If the size falls within the browser's maximum explicit size for a single element, we can 17984 // set the size and be done. Otherwise, we have to create children that add up the the desired 17985 // size. 17986 if (size < MAX_ELEMENT_SIZE) { 17987 this.sizer.style[dimension] = size + 'px'; 17988 } else { 17989 this.sizer.style[dimension] = 'auto'; 17990 this.sizer.style[crossDimension] = 'auto'; 17991 17992 // Divide the total size we have to render into N max-size pieces. 17993 var numChildren = Math.floor(size / MAX_ELEMENT_SIZE); 17994 17995 // Element template to clone for each max-size piece. 17996 var sizerChild = document.createElement('div'); 17997 sizerChild.style[dimension] = MAX_ELEMENT_SIZE + 'px'; 17998 sizerChild.style[crossDimension] = '1px'; 17999 18000 for (var i = 0; i < numChildren; i++) { 18001 this.sizer.appendChild(sizerChild.cloneNode(false)); 18002 } 18003 18004 // Re-use the element template for the remainder. 18005 sizerChild.style[dimension] = (size - (numChildren * MAX_ELEMENT_SIZE)) + 'px'; 18006 this.sizer.appendChild(sizerChild); 18007 } 18008 }; 18009 18010 18011 /** 18012 * If auto-shrinking is enabled, shrinks or unshrinks as appropriate. 18013 * @private 18014 * @param {number} size The new size. 18015 */ 18016 VirtualRepeatContainerController.prototype.autoShrink_ = function(size) { 18017 var shrinkSize = Math.max(size, this.autoShrinkMin * this.repeater.getItemSize()); 18018 if (this.autoShrink && shrinkSize !== this.size) { 18019 if (this.oldElementSize === null) { 18020 this.oldElementSize = this.$element[0].style[this.getDimensionName_()]; 18021 } 18022 18023 var currentSize = this.originalSize || this.size; 18024 if (!currentSize || shrinkSize < currentSize) { 18025 if (!this.originalSize) { 18026 this.originalSize = this.size; 18027 } 18028 18029 this.setSize_(shrinkSize); 18030 } else if (this.originalSize !== null) { 18031 this.unsetSize_(); 18032 this.originalSize = null; 18033 this.updateSize(); 18034 } 18035 18036 this.repeater.containerUpdated(); 18037 } 18038 }; 18039 18040 18041 /** 18042 * Sets the scrollHeight or scrollWidth. Called by the repeater based on 18043 * its item count and item size. 18044 * @param {number} itemsSize The total size of the items. 18045 */ 18046 VirtualRepeatContainerController.prototype.setScrollSize = function(itemsSize) { 18047 var size = itemsSize + this.offsetSize; 18048 if (this.scrollSize === size) return; 18049 18050 this.sizeScroller_(size); 18051 this.autoShrink_(size); 18052 this.scrollSize = size; 18053 }; 18054 18055 18056 /** @return {number} The container's current scroll offset. */ 18057 VirtualRepeatContainerController.prototype.getScrollOffset = function() { 18058 return this.scrollOffset; 18059 }; 18060 18061 /** 18062 * Scrolls to a given scrollTop position. 18063 * @param {number} position 18064 */ 18065 VirtualRepeatContainerController.prototype.scrollTo = function(position) { 18066 this.scroller[this.isHorizontal() ? 'scrollLeft' : 'scrollTop'] = position; 18067 this.handleScroll_(); 18068 }; 18069 18070 /** 18071 * Scrolls the item with the given index to the top of the scroll container. 18072 * @param {number} index 18073 */ 18074 VirtualRepeatContainerController.prototype.scrollToIndex = function(index) { 18075 var itemSize = this.repeater.getItemSize(); 18076 var itemsLength = this.repeater.itemsLength; 18077 if(index > itemsLength) { 18078 index = itemsLength - 1; 18079 } 18080 this.scrollTo(itemSize * index); 18081 }; 18082 18083 VirtualRepeatContainerController.prototype.resetScroll = function() { 18084 this.scrollTo(0); 18085 }; 18086 18087 18088 VirtualRepeatContainerController.prototype.handleScroll_ = function() { 18089 var offset = this.isHorizontal() ? this.scroller.scrollLeft : this.scroller.scrollTop; 18090 if (offset === this.scrollOffset || offset > this.scrollSize - this.size) return; 18091 18092 var itemSize = this.repeater.getItemSize(); 18093 if (!itemSize) return; 18094 18095 var numItems = Math.max(0, Math.floor(offset / itemSize) - NUM_EXTRA); 18096 18097 var transform = (this.isHorizontal() ? 'translateX(' : 'translateY(') + 18098 (numItems * itemSize) + 'px)'; 18099 18100 this.scrollOffset = offset; 18101 this.offsetter.style.webkitTransform = transform; 18102 this.offsetter.style.transform = transform; 18103 18104 if (this.bindTopIndex) { 18105 var topIndex = Math.floor(offset / itemSize); 18106 if (topIndex !== this.topIndex && topIndex < this.repeater.getItemCount()) { 18107 this.topIndex = topIndex; 18108 this.bindTopIndex.assign(this.$scope, topIndex); 18109 if (!this.$rootScope.$$phase) this.$scope.$digest(); 18110 } 18111 } 18112 18113 this.repeater.containerUpdated(); 18114 }; 18115 18116 18117 /** 18118 * @ngdoc directive 18119 * @name mdVirtualRepeat 18120 * @module material.components.virtualRepeat 18121 * @restrict A 18122 * @priority 1000 18123 * @description 18124 * `md-virtual-repeat` specifies an element to repeat using virtual scrolling. 18125 * 18126 * Virtual repeat is a limited substitute for ng-repeat that renders only 18127 * enough dom nodes to fill the container and recycling them as the user scrolls. 18128 * Arrays, but not objects are supported for iteration. 18129 * Track by, as alias, and (key, value) syntax are not supported. 18130 * 18131 * @usage 18132 * <hljs lang="html"> 18133 * <md-virtual-repeat-container> 18134 * <div md-virtual-repeat="i in items">Hello {{i}}!</div> 18135 * </md-virtual-repeat-container> 18136 * 18137 * <md-virtual-repeat-container md-orient-horizontal> 18138 * <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div> 18139 * </md-virtual-repeat-container> 18140 * </hljs> 18141 * 18142 * @param {number=} md-item-size The height or width of the repeated elements (which must be 18143 * identical for each element). Optional. Will attempt to read the size from the dom if missing, 18144 * but still assumes that all repeated nodes have same height or width. 18145 * @param {string=} md-extra-name Evaluates to an additional name to which the current iterated item 18146 * can be assigned on the repeated scope (needed for use in `md-autocomplete`). 18147 * @param {boolean=} md-on-demand When present, treats the md-virtual-repeat argument as an object 18148 * that can fetch rows rather than an array. 18149 * 18150 * **NOTE:** This object must implement the following interface with two (2) methods: 18151 * 18152 * - `getItemAtIndex: function(index) [object]` The item at that index or null if it is not yet 18153 * loaded (it should start downloading the item in that case). 18154 * - `getLength: function() [number]` The data length to which the repeater container 18155 * should be sized. Ideally, when the count is known, this method should return it. 18156 * Otherwise, return a higher number than the currently loaded items to produce an 18157 * infinite-scroll behavior. 18158 */ 18159 function VirtualRepeatDirective($parse) { 18160 return { 18161 controller: VirtualRepeatController, 18162 priority: 1000, 18163 require: ['mdVirtualRepeat', '^^mdVirtualRepeatContainer'], 18164 restrict: 'A', 18165 terminal: true, 18166 transclude: 'element', 18167 compile: function VirtualRepeatCompile($element, $attrs) { 18168 var expression = $attrs.mdVirtualRepeat; 18169 var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/); 18170 var repeatName = match[1]; 18171 var repeatListExpression = $parse(match[2]); 18172 var extraName = $attrs.mdExtraName && $parse($attrs.mdExtraName); 18173 18174 return function VirtualRepeatLink($scope, $element, $attrs, ctrl, $transclude) { 18175 ctrl[0].link_(ctrl[1], $transclude, repeatName, repeatListExpression, extraName); 18176 }; 18177 } 18178 }; 18179 } 18180 VirtualRepeatDirective.$inject = ["$parse"]; 18181 18182 18183 /** @ngInject */ 18184 function VirtualRepeatController($scope, $element, $attrs, $browser, $document, $rootScope, 18185 $$rAF) { 18186 this.$scope = $scope; 18187 this.$element = $element; 18188 this.$attrs = $attrs; 18189 this.$browser = $browser; 18190 this.$document = $document; 18191 this.$rootScope = $rootScope; 18192 this.$$rAF = $$rAF; 18193 18194 /** @type {boolean} Whether we are in on-demand mode. */ 18195 this.onDemand = $attrs.hasOwnProperty('mdOnDemand'); 18196 /** @type {!Function} Backup reference to $browser.$$checkUrlChange */ 18197 this.browserCheckUrlChange = $browser.$$checkUrlChange; 18198 /** @type {number} Most recent starting repeat index (based on scroll offset) */ 18199 this.newStartIndex = 0; 18200 /** @type {number} Most recent ending repeat index (based on scroll offset) */ 18201 this.newEndIndex = 0; 18202 /** @type {number} Most recent end visible index (based on scroll offset) */ 18203 this.newVisibleEnd = 0; 18204 /** @type {number} Previous starting repeat index (based on scroll offset) */ 18205 this.startIndex = 0; 18206 /** @type {number} Previous ending repeat index (based on scroll offset) */ 18207 this.endIndex = 0; 18208 // TODO: measure width/height of first element from dom if not provided. 18209 // getComputedStyle? 18210 /** @type {?number} Height/width of repeated elements. */ 18211 this.itemSize = $scope.$eval($attrs.mdItemSize) || null; 18212 18213 /** @type {boolean} Whether this is the first time that items are rendered. */ 18214 this.isFirstRender = true; 18215 18216 /** 18217 * @private {boolean} Whether the items in the list are already being updated. Used to prevent 18218 * nested calls to virtualRepeatUpdate_. 18219 */ 18220 this.isVirtualRepeatUpdating_ = false; 18221 18222 /** @type {number} Most recently seen length of items. */ 18223 this.itemsLength = 0; 18224 18225 /** 18226 * @type {!Function} Unwatch callback for item size (when md-items-size is 18227 * not specified), or angular.noop otherwise. 18228 */ 18229 this.unwatchItemSize_ = angular.noop; 18230 18231 /** 18232 * Presently rendered blocks by repeat index. 18233 * @type {Object<number, !VirtualRepeatController.Block} 18234 */ 18235 this.blocks = {}; 18236 /** @type {Array<!VirtualRepeatController.Block>} A pool of presently unused blocks. */ 18237 this.pooledBlocks = []; 18238 } 18239 VirtualRepeatController.$inject = ["$scope", "$element", "$attrs", "$browser", "$document", "$rootScope", "$$rAF"]; 18240 18241 18242 /** 18243 * An object representing a repeated item. 18244 * @typedef {{element: !jqLite, new: boolean, scope: !angular.Scope}} 18245 */ 18246 VirtualRepeatController.Block; 18247 18248 18249 /** 18250 * Called at startup by the md-virtual-repeat postLink function. 18251 * @param {!VirtualRepeatContainerController} container The container's controller. 18252 * @param {!Function} transclude The repeated element's bound transclude function. 18253 * @param {string} repeatName The left hand side of the repeat expression, indicating 18254 * the name for each item in the array. 18255 * @param {!Function} repeatListExpression A compiled expression based on the right hand side 18256 * of the repeat expression. Points to the array to repeat over. 18257 * @param {string|undefined} extraName The optional extra repeatName. 18258 */ 18259 VirtualRepeatController.prototype.link_ = 18260 function(container, transclude, repeatName, repeatListExpression, extraName) { 18261 this.container = container; 18262 this.transclude = transclude; 18263 this.repeatName = repeatName; 18264 this.rawRepeatListExpression = repeatListExpression; 18265 this.extraName = extraName; 18266 this.sized = false; 18267 18268 this.repeatListExpression = angular.bind(this, this.repeatListExpression_); 18269 18270 this.container.register(this); 18271 }; 18272 18273 18274 /** @private Attempts to set itemSize by measuring a repeated element in the dom */ 18275 VirtualRepeatController.prototype.readItemSize_ = function() { 18276 if (this.itemSize) { 18277 // itemSize was successfully read in a different asynchronous call. 18278 return; 18279 } 18280 18281 this.items = this.repeatListExpression(this.$scope); 18282 this.parentNode = this.$element[0].parentNode; 18283 var block = this.getBlock_(0); 18284 if (!block.element[0].parentNode) { 18285 this.parentNode.appendChild(block.element[0]); 18286 } 18287 18288 this.itemSize = block.element[0][ 18289 this.container.isHorizontal() ? 'offsetWidth' : 'offsetHeight'] || null; 18290 18291 this.blocks[0] = block; 18292 this.poolBlock_(0); 18293 18294 if (this.itemSize) { 18295 this.containerUpdated(); 18296 } 18297 }; 18298 18299 18300 /** 18301 * Returns the user-specified repeat list, transforming it into an array-like 18302 * object in the case of infinite scroll/dynamic load mode. 18303 * @param {!angular.Scope} The scope. 18304 * @return {!Array|!Object} An array or array-like object for iteration. 18305 */ 18306 VirtualRepeatController.prototype.repeatListExpression_ = function(scope) { 18307 var repeatList = this.rawRepeatListExpression(scope); 18308 18309 if (this.onDemand && repeatList) { 18310 var virtualList = new VirtualRepeatModelArrayLike(repeatList); 18311 virtualList.$$includeIndexes(this.newStartIndex, this.newVisibleEnd); 18312 return virtualList; 18313 } else { 18314 return repeatList; 18315 } 18316 }; 18317 18318 18319 /** 18320 * Called by the container. Informs us that the containers scroll or size has 18321 * changed. 18322 */ 18323 VirtualRepeatController.prototype.containerUpdated = function() { 18324 // If itemSize is unknown, attempt to measure it. 18325 if (!this.itemSize) { 18326 this.unwatchItemSize_ = this.$scope.$watchCollection( 18327 this.repeatListExpression, 18328 angular.bind(this, function(items) { 18329 if (items && items.length) { 18330 this.$$rAF(angular.bind(this, this.readItemSize_)); 18331 } 18332 })); 18333 if (!this.$rootScope.$$phase) this.$scope.$digest(); 18334 18335 return; 18336 } else if (!this.sized) { 18337 this.items = this.repeatListExpression(this.$scope); 18338 } 18339 18340 if (!this.sized) { 18341 this.unwatchItemSize_(); 18342 this.sized = true; 18343 this.$scope.$watchCollection(this.repeatListExpression, 18344 angular.bind(this, function(items, oldItems) { 18345 if (!this.isVirtualRepeatUpdating_) { 18346 this.virtualRepeatUpdate_(items, oldItems); 18347 } 18348 })); 18349 } 18350 18351 this.updateIndexes_(); 18352 18353 if (this.newStartIndex !== this.startIndex || 18354 this.newEndIndex !== this.endIndex || 18355 this.container.getScrollOffset() > this.container.getScrollSize()) { 18356 if (this.items instanceof VirtualRepeatModelArrayLike) { 18357 this.items.$$includeIndexes(this.newStartIndex, this.newEndIndex); 18358 } 18359 this.virtualRepeatUpdate_(this.items, this.items); 18360 } 18361 }; 18362 18363 18364 /** 18365 * Called by the container. Returns the size of a single repeated item. 18366 * @return {?number} Size of a repeated item. 18367 */ 18368 VirtualRepeatController.prototype.getItemSize = function() { 18369 return this.itemSize; 18370 }; 18371 18372 18373 /** 18374 * Called by the container. Returns the size of a single repeated item. 18375 * @return {?number} Size of a repeated item. 18376 */ 18377 VirtualRepeatController.prototype.getItemCount = function() { 18378 return this.itemsLength; 18379 }; 18380 18381 18382 /** 18383 * Updates the order and visible offset of repeated blocks in response to scrolling 18384 * or items updates. 18385 * @private 18386 */ 18387 VirtualRepeatController.prototype.virtualRepeatUpdate_ = function(items, oldItems) { 18388 this.isVirtualRepeatUpdating_ = true; 18389 18390 var itemsLength = items && items.length || 0; 18391 var lengthChanged = false; 18392 18393 // If the number of items shrank, scroll up to the top. 18394 if (this.items && itemsLength < this.items.length && this.container.getScrollOffset() !== 0) { 18395 this.items = items; 18396 this.container.resetScroll(); 18397 return; 18398 } 18399 18400 if (itemsLength !== this.itemsLength) { 18401 lengthChanged = true; 18402 this.itemsLength = itemsLength; 18403 } 18404 18405 this.items = items; 18406 if (items !== oldItems || lengthChanged) { 18407 this.updateIndexes_(); 18408 } 18409 18410 this.parentNode = this.$element[0].parentNode; 18411 18412 if (lengthChanged) { 18413 this.container.setScrollSize(itemsLength * this.itemSize); 18414 } 18415 18416 if (this.isFirstRender) { 18417 this.isFirstRender = false; 18418 var startIndex = this.$attrs.mdStartIndex ? 18419 this.$scope.$eval(this.$attrs.mdStartIndex) : 18420 this.container.topIndex; 18421 this.container.scrollToIndex(startIndex); 18422 } 18423 18424 // Detach and pool any blocks that are no longer in the viewport. 18425 Object.keys(this.blocks).forEach(function(blockIndex) { 18426 var index = parseInt(blockIndex, 10); 18427 if (index < this.newStartIndex || index >= this.newEndIndex) { 18428 this.poolBlock_(index); 18429 } 18430 }, this); 18431 18432 // Add needed blocks. 18433 // For performance reasons, temporarily block browser url checks as we digest 18434 // the restored block scopes ($$checkUrlChange reads window.location to 18435 // check for changes and trigger route change, etc, which we don't need when 18436 // trying to scroll at 60fps). 18437 this.$browser.$$checkUrlChange = angular.noop; 18438 18439 var i, block, 18440 newStartBlocks = [], 18441 newEndBlocks = []; 18442 18443 // Collect blocks at the top. 18444 for (i = this.newStartIndex; i < this.newEndIndex && this.blocks[i] == null; i++) { 18445 block = this.getBlock_(i); 18446 this.updateBlock_(block, i); 18447 newStartBlocks.push(block); 18448 } 18449 18450 // Update blocks that are already rendered. 18451 for (; this.blocks[i] != null; i++) { 18452 this.updateBlock_(this.blocks[i], i); 18453 } 18454 var maxIndex = i - 1; 18455 18456 // Collect blocks at the end. 18457 for (; i < this.newEndIndex; i++) { 18458 block = this.getBlock_(i); 18459 this.updateBlock_(block, i); 18460 newEndBlocks.push(block); 18461 } 18462 18463 // Attach collected blocks to the document. 18464 if (newStartBlocks.length) { 18465 this.parentNode.insertBefore( 18466 this.domFragmentFromBlocks_(newStartBlocks), 18467 this.$element[0].nextSibling); 18468 } 18469 if (newEndBlocks.length) { 18470 this.parentNode.insertBefore( 18471 this.domFragmentFromBlocks_(newEndBlocks), 18472 this.blocks[maxIndex] && this.blocks[maxIndex].element[0].nextSibling); 18473 } 18474 18475 // Restore $$checkUrlChange. 18476 this.$browser.$$checkUrlChange = this.browserCheckUrlChange; 18477 18478 this.startIndex = this.newStartIndex; 18479 this.endIndex = this.newEndIndex; 18480 18481 this.isVirtualRepeatUpdating_ = false; 18482 }; 18483 18484 18485 /** 18486 * @param {number} index Where the block is to be in the repeated list. 18487 * @return {!VirtualRepeatController.Block} A new or pooled block to place at the specified index. 18488 * @private 18489 */ 18490 VirtualRepeatController.prototype.getBlock_ = function(index) { 18491 if (this.pooledBlocks.length) { 18492 return this.pooledBlocks.pop(); 18493 } 18494 18495 var block; 18496 this.transclude(angular.bind(this, function(clone, scope) { 18497 block = { 18498 element: clone, 18499 new: true, 18500 scope: scope 18501 }; 18502 18503 this.updateScope_(scope, index); 18504 this.parentNode.appendChild(clone[0]); 18505 })); 18506 18507 return block; 18508 }; 18509 18510 18511 /** 18512 * Updates and if not in a digest cycle, digests the specified block's scope to the data 18513 * at the specified index. 18514 * @param {!VirtualRepeatController.Block} block The block whose scope should be updated. 18515 * @param {number} index The index to set. 18516 * @private 18517 */ 18518 VirtualRepeatController.prototype.updateBlock_ = function(block, index) { 18519 this.blocks[index] = block; 18520 18521 if (!block.new && 18522 (block.scope.$index === index && block.scope[this.repeatName] === this.items[index])) { 18523 return; 18524 } 18525 block.new = false; 18526 18527 // Update and digest the block's scope. 18528 this.updateScope_(block.scope, index); 18529 18530 // Perform digest before reattaching the block. 18531 // Any resulting synchronous dom mutations should be much faster as a result. 18532 // This might break some directives, but I'm going to try it for now. 18533 if (!this.$rootScope.$$phase) { 18534 block.scope.$digest(); 18535 } 18536 }; 18537 18538 18539 /** 18540 * Updates scope to the data at the specified index. 18541 * @param {!angular.Scope} scope The scope which should be updated. 18542 * @param {number} index The index to set. 18543 * @private 18544 */ 18545 VirtualRepeatController.prototype.updateScope_ = function(scope, index) { 18546 scope.$index = index; 18547 scope[this.repeatName] = this.items && this.items[index]; 18548 if (this.extraName) scope[this.extraName(this.$scope)] = this.items[index]; 18549 }; 18550 18551 18552 /** 18553 * Pools the block at the specified index (Pulls its element out of the dom and stores it). 18554 * @param {number} index The index at which the block to pool is stored. 18555 * @private 18556 */ 18557 VirtualRepeatController.prototype.poolBlock_ = function(index) { 18558 this.pooledBlocks.push(this.blocks[index]); 18559 this.parentNode.removeChild(this.blocks[index].element[0]); 18560 delete this.blocks[index]; 18561 }; 18562 18563 18564 /** 18565 * Produces a dom fragment containing the elements from the list of blocks. 18566 * @param {!Array<!VirtualRepeatController.Block>} blocks The blocks whose elements 18567 * should be added to the document fragment. 18568 * @return {DocumentFragment} 18569 * @private 18570 */ 18571 VirtualRepeatController.prototype.domFragmentFromBlocks_ = function(blocks) { 18572 var fragment = this.$document[0].createDocumentFragment(); 18573 blocks.forEach(function(block) { 18574 fragment.appendChild(block.element[0]); 18575 }); 18576 return fragment; 18577 }; 18578 18579 18580 /** 18581 * Updates start and end indexes based on length of repeated items and container size. 18582 * @private 18583 */ 18584 VirtualRepeatController.prototype.updateIndexes_ = function() { 18585 var itemsLength = this.items ? this.items.length : 0; 18586 var containerLength = Math.ceil(this.container.getSize() / this.itemSize); 18587 18588 this.newStartIndex = Math.max(0, Math.min( 18589 itemsLength - containerLength, 18590 Math.floor(this.container.getScrollOffset() / this.itemSize))); 18591 this.newVisibleEnd = this.newStartIndex + containerLength + NUM_EXTRA; 18592 this.newEndIndex = Math.min(itemsLength, this.newVisibleEnd); 18593 this.newStartIndex = Math.max(0, this.newStartIndex - NUM_EXTRA); 18594 }; 18595 18596 /** 18597 * This VirtualRepeatModelArrayLike class enforces the interface requirements 18598 * for infinite scrolling within a mdVirtualRepeatContainer. An object with this 18599 * interface must implement the following interface with two (2) methods: 18600 * 18601 * getItemAtIndex: function(index) -> item at that index or null if it is not yet 18602 * loaded (It should start downloading the item in that case). 18603 * 18604 * getLength: function() -> number The data legnth to which the repeater container 18605 * should be sized. Ideally, when the count is known, this method should return it. 18606 * Otherwise, return a higher number than the currently loaded items to produce an 18607 * infinite-scroll behavior. 18608 * 18609 * @usage 18610 * <hljs lang="html"> 18611 * <md-virtual-repeat-container md-orient-horizontal> 18612 * <div md-virtual-repeat="i in items" md-on-demand> 18613 * Hello {{i}}! 18614 * </div> 18615 * </md-virtual-repeat-container> 18616 * </hljs> 18617 * 18618 */ 18619 function VirtualRepeatModelArrayLike(model) { 18620 if (!angular.isFunction(model.getItemAtIndex) || 18621 !angular.isFunction(model.getLength)) { 18622 throw Error('When md-on-demand is enabled, the Object passed to md-virtual-repeat must implement ' + 18623 'functions getItemAtIndex() and getLength() '); 18624 } 18625 18626 this.model = model; 18627 } 18628 18629 18630 VirtualRepeatModelArrayLike.prototype.$$includeIndexes = function(start, end) { 18631 for (var i = start; i < end; i++) { 18632 if (!this.hasOwnProperty(i)) { 18633 this[i] = this.model.getItemAtIndex(i); 18634 } 18635 } 18636 this.length = this.model.getLength(); 18637 }; 18638 18639 18640 function abstractMethod() { 18641 throw Error('Non-overridden abstract method called.'); 18642 } 18643 18644 })(); 18645 (function(){ 18646 "use strict"; 18647 18648 /** 18649 * @ngdoc module 18650 * @name material.components.whiteframe 18651 */ 18652 angular 18653 .module('material.components.whiteframe', ['material.core']) 18654 .directive('mdWhiteframe', MdWhiteframeDirective); 18655 18656 /** 18657 * @private 18658 * @ngdoc directive 18659 * @module material.components.whiteframe 18660 * @name mdWhiteframe 18661 * @restrict A 18662 * 18663 * @description 18664 * The md-whiteframe directive allows you to apply an elevation shadow to an element. 18665 * 18666 * The attribute values needs to be a number between 1 and 24. 18667 * 18668 * ### Notes 18669 * - If there is no value specified it defaults to 4dp. 18670 * - If the value is not valid it defaults to 4dp. 18671 18672 * @usage 18673 * <hljs lang="html"> 18674 * <div md-whiteframe="3"> 18675 * <span>Elevation of 3dp</span> 18676 * </div> 18677 * </hljs> 18678 */ 18679 function MdWhiteframeDirective($log) { 18680 var MIN_DP = 1; 18681 var MAX_DP = 24; 18682 var DEFAULT_DP = 4; 18683 18684 return { 18685 restrict: 'A', 18686 link: postLink 18687 }; 18688 18689 function postLink(scope, element, attr) { 18690 var elevation = parseInt(attr.mdWhiteframe, 10) || DEFAULT_DP; 18691 18692 if (elevation > MAX_DP || elevation < MIN_DP) { 18693 $log.warn('md-whiteframe attribute value is invalid. It should be a number between ' + MIN_DP + ' and ' + MAX_DP, element[0]); 18694 elevation = DEFAULT_DP; 18695 } 18696 18697 element.addClass('md-whiteframe-' + elevation + 'dp'); 18698 } 18699 } 18700 MdWhiteframeDirective.$inject = ["$log"]; 18701 18702 18703 })(); 18704 (function(){ 18705 "use strict"; 18706 18707 angular 18708 .module('material.components.autocomplete') 18709 .controller('MdAutocompleteCtrl', MdAutocompleteCtrl); 18710 18711 var ITEM_HEIGHT = 41, 18712 MAX_HEIGHT = 5.5 * ITEM_HEIGHT, 18713 MENU_PADDING = 8, 18714 INPUT_PADDING = 2; // Padding provided by `md-input-container` 18715 18716 function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window, 18717 $animate, $rootElement, $attrs, $q) { 18718 //-- private variables 18719 var ctrl = this, 18720 itemParts = $scope.itemsExpr.split(/ in /i), 18721 itemExpr = itemParts[ 1 ], 18722 elements = null, 18723 cache = {}, 18724 noBlur = false, 18725 selectedItemWatchers = [], 18726 hasFocus = false, 18727 lastCount = 0, 18728 fetchesInProgress = 0; 18729 18730 //-- public variables with handlers 18731 defineProperty('hidden', handleHiddenChange, true); 18732 18733 //-- public variables 18734 ctrl.scope = $scope; 18735 ctrl.parent = $scope.$parent; 18736 ctrl.itemName = itemParts[ 0 ]; 18737 ctrl.matches = []; 18738 ctrl.loading = false; 18739 ctrl.hidden = true; 18740 ctrl.index = null; 18741 ctrl.messages = []; 18742 ctrl.id = $mdUtil.nextUid(); 18743 ctrl.isDisabled = null; 18744 ctrl.isRequired = null; 18745 ctrl.isReadonly = null; 18746 ctrl.hasNotFound = false; 18747 18748 //-- public methods 18749 ctrl.keydown = keydown; 18750 ctrl.blur = blur; 18751 ctrl.focus = focus; 18752 ctrl.clear = clearValue; 18753 ctrl.select = select; 18754 ctrl.listEnter = onListEnter; 18755 ctrl.listLeave = onListLeave; 18756 ctrl.mouseUp = onMouseup; 18757 ctrl.getCurrentDisplayValue = getCurrentDisplayValue; 18758 ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher; 18759 ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher; 18760 ctrl.notFoundVisible = notFoundVisible; 18761 ctrl.loadingIsVisible = loadingIsVisible; 18762 18763 return init(); 18764 18765 //-- initialization methods 18766 18767 /** 18768 * Initialize the controller, setup watchers, gather elements 18769 */ 18770 function init () { 18771 $mdUtil.initOptionalProperties($scope, $attrs, { searchText: null, selectedItem: null }); 18772 $mdTheming($element); 18773 configureWatchers(); 18774 $mdUtil.nextTick(function () { 18775 gatherElements(); 18776 moveDropdown(); 18777 focusElement(); 18778 $element.on('focus', focusElement); 18779 }); 18780 } 18781 18782 /** 18783 * Calculates the dropdown's position and applies the new styles to the menu element 18784 * @returns {*} 18785 */ 18786 function positionDropdown () { 18787 if (!elements) return $mdUtil.nextTick(positionDropdown, false, $scope); 18788 var hrect = elements.wrap.getBoundingClientRect(), 18789 vrect = elements.snap.getBoundingClientRect(), 18790 root = elements.root.getBoundingClientRect(), 18791 top = vrect.bottom - root.top, 18792 bot = root.bottom - vrect.top, 18793 left = hrect.left - root.left, 18794 width = hrect.width, 18795 offset = getVerticalOffset(), 18796 styles; 18797 // Adjust the width to account for the padding provided by `md-input-container` 18798 if ($attrs.mdFloatingLabel) { 18799 left += INPUT_PADDING; 18800 width -= INPUT_PADDING * 2; 18801 } 18802 styles = { 18803 left: left + 'px', 18804 minWidth: width + 'px', 18805 maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px' 18806 }; 18807 if (top > bot && root.height - hrect.bottom - MENU_PADDING < MAX_HEIGHT) { 18808 styles.top = 'auto'; 18809 styles.bottom = bot + 'px'; 18810 styles.maxHeight = Math.min(MAX_HEIGHT, hrect.top - root.top - MENU_PADDING) + 'px'; 18811 } else { 18812 styles.top = (top - offset) + 'px'; 18813 styles.bottom = 'auto'; 18814 styles.maxHeight = Math.min(MAX_HEIGHT, root.bottom + $mdUtil.scrollTop() - hrect.bottom - MENU_PADDING) + 'px'; 18815 } 18816 18817 elements.$.scrollContainer.css(styles); 18818 $mdUtil.nextTick(correctHorizontalAlignment, false); 18819 18820 /** 18821 * Calculates the vertical offset for floating label examples to account for ngMessages 18822 * @returns {number} 18823 */ 18824 function getVerticalOffset () { 18825 var offset = 0; 18826 var inputContainer = $element.find('md-input-container'); 18827 if (inputContainer.length) { 18828 var input = inputContainer.find('input'); 18829 offset = inputContainer.prop('offsetHeight'); 18830 offset -= input.prop('offsetTop'); 18831 offset -= input.prop('offsetHeight'); 18832 // add in the height left up top for the floating label text 18833 offset += inputContainer.prop('offsetTop'); 18834 } 18835 return offset; 18836 } 18837 18838 /** 18839 * Makes sure that the menu doesn't go off of the screen on either side. 18840 */ 18841 function correctHorizontalAlignment () { 18842 var dropdown = elements.scrollContainer.getBoundingClientRect(), 18843 styles = {}; 18844 if (dropdown.right > root.right - MENU_PADDING) { 18845 styles.left = (hrect.right - dropdown.width) + 'px'; 18846 } 18847 elements.$.scrollContainer.css(styles); 18848 } 18849 } 18850 18851 /** 18852 * Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues. 18853 */ 18854 function moveDropdown () { 18855 if (!elements.$.root.length) return; 18856 $mdTheming(elements.$.scrollContainer); 18857 elements.$.scrollContainer.detach(); 18858 elements.$.root.append(elements.$.scrollContainer); 18859 if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement); 18860 } 18861 18862 /** 18863 * Sends focus to the input element. 18864 */ 18865 function focusElement () { 18866 if ($scope.autofocus) elements.input.focus(); 18867 } 18868 18869 /** 18870 * Sets up any watchers used by autocomplete 18871 */ 18872 function configureWatchers () { 18873 var wait = parseInt($scope.delay, 10) || 0; 18874 $attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); }); 18875 $attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); }); 18876 $attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); }); 18877 $scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText); 18878 $scope.$watch('selectedItem', selectedItemChange); 18879 angular.element($window).on('resize', positionDropdown); 18880 $scope.$on('$destroy', cleanup); 18881 } 18882 18883 /** 18884 * Removes any events or leftover elements created by this controller 18885 */ 18886 function cleanup () { 18887 if(!ctrl.hidden) { 18888 $mdUtil.enableScrolling(); 18889 } 18890 18891 angular.element($window).off('resize', positionDropdown); 18892 if ( elements ){ 18893 var items = 'ul scroller scrollContainer input'.split(' '); 18894 angular.forEach(items, function(key){ 18895 elements.$[key].remove(); 18896 }); 18897 } 18898 } 18899 18900 /** 18901 * Gathers all of the elements needed for this controller 18902 */ 18903 function gatherElements () { 18904 elements = { 18905 main: $element[0], 18906 scrollContainer: $element[0].getElementsByClassName('md-virtual-repeat-container')[0], 18907 scroller: $element[0].getElementsByClassName('md-virtual-repeat-scroller')[0], 18908 ul: $element.find('ul')[0], 18909 input: $element.find('input')[0], 18910 wrap: $element.find('md-autocomplete-wrap')[0], 18911 root: document.body 18912 }; 18913 elements.li = elements.ul.getElementsByTagName('li'); 18914 elements.snap = getSnapTarget(); 18915 elements.$ = getAngularElements(elements); 18916 } 18917 18918 /** 18919 * Finds the element that the menu will base its position on 18920 * @returns {*} 18921 */ 18922 function getSnapTarget () { 18923 for (var element = $element; element.length; element = element.parent()) { 18924 if (angular.isDefined(element.attr('md-autocomplete-snap'))) return element[ 0 ]; 18925 } 18926 return elements.wrap; 18927 } 18928 18929 /** 18930 * Gathers angular-wrapped versions of each element 18931 * @param elements 18932 * @returns {{}} 18933 */ 18934 function getAngularElements (elements) { 18935 var obj = {}; 18936 for (var key in elements) { 18937 if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]); 18938 } 18939 return obj; 18940 } 18941 18942 //-- event/change handlers 18943 18944 /** 18945 * Handles changes to the `hidden` property. 18946 * @param hidden 18947 * @param oldHidden 18948 */ 18949 function handleHiddenChange (hidden, oldHidden) { 18950 if (!hidden && oldHidden) { 18951 positionDropdown(); 18952 18953 if (elements) { 18954 $mdUtil.nextTick(function () { 18955 $mdUtil.disableScrollAround(elements.ul); 18956 }, false, $scope); 18957 } 18958 } else if (hidden && !oldHidden) { 18959 $mdUtil.nextTick(function () { 18960 $mdUtil.enableScrolling(); 18961 }, false, $scope); 18962 } 18963 } 18964 18965 /** 18966 * When the user mouses over the dropdown menu, ignore blur events. 18967 */ 18968 function onListEnter () { 18969 noBlur = true; 18970 } 18971 18972 /** 18973 * When the user's mouse leaves the menu, blur events may hide the menu again. 18974 */ 18975 function onListLeave () { 18976 if (!hasFocus) elements.input.focus(); 18977 noBlur = false; 18978 ctrl.hidden = shouldHide(); 18979 } 18980 18981 /** 18982 * When the mouse button is released, send focus back to the input field. 18983 */ 18984 function onMouseup () { 18985 elements.input.focus(); 18986 } 18987 18988 /** 18989 * Handles changes to the selected item. 18990 * @param selectedItem 18991 * @param previousSelectedItem 18992 */ 18993 function selectedItemChange (selectedItem, previousSelectedItem) { 18994 if (selectedItem) { 18995 getDisplayValue(selectedItem).then(function (val) { 18996 $scope.searchText = val; 18997 handleSelectedItemChange(selectedItem, previousSelectedItem); 18998 }); 18999 } 19000 19001 if (selectedItem !== previousSelectedItem) announceItemChange(); 19002 } 19003 19004 /** 19005 * Use the user-defined expression to announce changes each time a new item is selected 19006 */ 19007 function announceItemChange () { 19008 angular.isFunction($scope.itemChange) && $scope.itemChange(getItemAsNameVal($scope.selectedItem)); 19009 } 19010 19011 /** 19012 * Use the user-defined expression to announce changes each time the search text is changed 19013 */ 19014 function announceTextChange () { 19015 angular.isFunction($scope.textChange) && $scope.textChange(); 19016 } 19017 19018 /** 19019 * Calls any external watchers listening for the selected item. Used in conjunction with 19020 * `registerSelectedItemWatcher`. 19021 * @param selectedItem 19022 * @param previousSelectedItem 19023 */ 19024 function handleSelectedItemChange (selectedItem, previousSelectedItem) { 19025 selectedItemWatchers.forEach(function (watcher) { watcher(selectedItem, previousSelectedItem); }); 19026 } 19027 19028 /** 19029 * Register a function to be called when the selected item changes. 19030 * @param cb 19031 */ 19032 function registerSelectedItemWatcher (cb) { 19033 if (selectedItemWatchers.indexOf(cb) == -1) { 19034 selectedItemWatchers.push(cb); 19035 } 19036 } 19037 19038 /** 19039 * Unregister a function previously registered for selected item changes. 19040 * @param cb 19041 */ 19042 function unregisterSelectedItemWatcher (cb) { 19043 var i = selectedItemWatchers.indexOf(cb); 19044 if (i != -1) { 19045 selectedItemWatchers.splice(i, 1); 19046 } 19047 } 19048 19049 /** 19050 * Handles changes to the searchText property. 19051 * @param searchText 19052 * @param previousSearchText 19053 */ 19054 function handleSearchText (searchText, previousSearchText) { 19055 ctrl.index = getDefaultIndex(); 19056 // do nothing on init 19057 if (searchText === previousSearchText) return; 19058 19059 getDisplayValue($scope.selectedItem).then(function (val) { 19060 // clear selected item if search text no longer matches it 19061 if (searchText !== val) { 19062 $scope.selectedItem = null; 19063 19064 // trigger change event if available 19065 if (searchText !== previousSearchText) announceTextChange(); 19066 19067 // cancel results if search text is not long enough 19068 if (!isMinLengthMet()) { 19069 ctrl.matches = []; 19070 setLoading(false); 19071 updateMessages(); 19072 } else { 19073 handleQuery(); 19074 } 19075 } 19076 }); 19077 19078 } 19079 19080 /** 19081 * Handles input blur event, determines if the dropdown should hide. 19082 */ 19083 function blur () { 19084 hasFocus = false; 19085 if (!noBlur) { 19086 ctrl.hidden = shouldHide(); 19087 } 19088 } 19089 19090 /** 19091 * Force blur on input element 19092 * @param forceBlur 19093 */ 19094 function doBlur(forceBlur) { 19095 if (forceBlur) { 19096 noBlur = false; 19097 hasFocus = false; 19098 } 19099 elements.input.blur(); 19100 } 19101 19102 /** 19103 * Handles input focus event, determines if the dropdown should show. 19104 */ 19105 function focus () { 19106 hasFocus = true; 19107 //-- if searchText is null, let's force it to be a string 19108 if (!angular.isString($scope.searchText)) $scope.searchText = ''; 19109 ctrl.hidden = shouldHide(); 19110 if (!ctrl.hidden) handleQuery(); 19111 } 19112 19113 /** 19114 * Handles keyboard input. 19115 * @param event 19116 */ 19117 function keydown (event) { 19118 switch (event.keyCode) { 19119 case $mdConstant.KEY_CODE.DOWN_ARROW: 19120 if (ctrl.loading) return; 19121 event.stopPropagation(); 19122 event.preventDefault(); 19123 ctrl.index = Math.min(ctrl.index + 1, ctrl.matches.length - 1); 19124 updateScroll(); 19125 updateMessages(); 19126 break; 19127 case $mdConstant.KEY_CODE.UP_ARROW: 19128 if (ctrl.loading) return; 19129 event.stopPropagation(); 19130 event.preventDefault(); 19131 ctrl.index = ctrl.index < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1); 19132 updateScroll(); 19133 updateMessages(); 19134 break; 19135 case $mdConstant.KEY_CODE.TAB: 19136 // If we hit tab, assume that we've left the list so it will close 19137 onListLeave(); 19138 19139 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return; 19140 select(ctrl.index); 19141 break; 19142 case $mdConstant.KEY_CODE.ENTER: 19143 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return; 19144 if (hasSelection()) return; 19145 event.stopPropagation(); 19146 event.preventDefault(); 19147 select(ctrl.index); 19148 break; 19149 case $mdConstant.KEY_CODE.ESCAPE: 19150 event.stopPropagation(); 19151 event.preventDefault(); 19152 clearValue(); 19153 19154 // Force the component to blur if they hit escape 19155 doBlur(true); 19156 19157 break; 19158 default: 19159 } 19160 } 19161 19162 //-- getters 19163 19164 /** 19165 * Returns the minimum length needed to display the dropdown. 19166 * @returns {*} 19167 */ 19168 function getMinLength () { 19169 return angular.isNumber($scope.minLength) ? $scope.minLength : 1; 19170 } 19171 19172 /** 19173 * Returns the display value for an item. 19174 * @param item 19175 * @returns {*} 19176 */ 19177 function getDisplayValue (item) { 19178 return $q.when(getItemText(item) || item); 19179 19180 /** 19181 * Getter function to invoke user-defined expression (in the directive) 19182 * to convert your object to a single string. 19183 */ 19184 function getItemText (item) { 19185 return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null; 19186 } 19187 } 19188 19189 /** 19190 * Returns the locals object for compiling item templates. 19191 * @param item 19192 * @returns {{}} 19193 */ 19194 function getItemAsNameVal (item) { 19195 if (!item) return undefined; 19196 19197 var locals = {}; 19198 if (ctrl.itemName) locals[ ctrl.itemName ] = item; 19199 19200 return locals; 19201 } 19202 19203 /** 19204 * Returns the default index based on whether or not autoselect is enabled. 19205 * @returns {number} 19206 */ 19207 function getDefaultIndex () { 19208 return $scope.autoselect ? 0 : -1; 19209 } 19210 19211 /** 19212 * Sets the loading parameter and updates the hidden state. 19213 * @param value {boolean} Whether or not the component is currently loading. 19214 */ 19215 function setLoading(value) { 19216 if (ctrl.loading != value) { 19217 ctrl.loading = value; 19218 } 19219 19220 // Always refresh the hidden variable as something else might have changed 19221 ctrl.hidden = shouldHide(); 19222 } 19223 19224 /** 19225 * Determines if the menu should be hidden. 19226 * @returns {boolean} 19227 */ 19228 function shouldHide () { 19229 if (ctrl.loading && !hasMatches()) return true; // Hide while loading initial matches 19230 else if (hasSelection()) return true; // Hide if there is already a selection 19231 else if (!hasFocus) return true; // Hide if the input does not have focus 19232 else return !shouldShow(); // Defer to standard show logic 19233 } 19234 19235 /** 19236 * Determines if the menu should be shown. 19237 * @returns {boolean} 19238 */ 19239 function shouldShow() { 19240 return (isMinLengthMet() && hasMatches()) || notFoundVisible(); 19241 } 19242 19243 /** 19244 * Returns true if the search text has matches. 19245 * @returns {boolean} 19246 */ 19247 function hasMatches() { 19248 return ctrl.matches.length ? true : false; 19249 } 19250 19251 /** 19252 * Returns true if the autocomplete has a valid selection. 19253 * @returns {boolean} 19254 */ 19255 function hasSelection() { 19256 return ctrl.scope.selectedItem ? true : false; 19257 } 19258 19259 /** 19260 * Returns true if the loading indicator is, or should be, visible. 19261 * @returns {boolean} 19262 */ 19263 function loadingIsVisible() { 19264 return ctrl.loading && !hasSelection(); 19265 } 19266 19267 /** 19268 * Returns the display value of the current item. 19269 * @returns {*} 19270 */ 19271 function getCurrentDisplayValue () { 19272 return getDisplayValue(ctrl.matches[ ctrl.index ]); 19273 } 19274 19275 /** 19276 * Determines if the minimum length is met by the search text. 19277 * @returns {*} 19278 */ 19279 function isMinLengthMet () { 19280 return ($scope.searchText || '').length >= getMinLength(); 19281 } 19282 19283 //-- actions 19284 19285 /** 19286 * Defines a public property with a handler and a default value. 19287 * @param key 19288 * @param handler 19289 * @param value 19290 */ 19291 function defineProperty (key, handler, value) { 19292 Object.defineProperty(ctrl, key, { 19293 get: function () { return value; }, 19294 set: function (newValue) { 19295 var oldValue = value; 19296 value = newValue; 19297 handler(newValue, oldValue); 19298 } 19299 }); 19300 } 19301 19302 /** 19303 * Selects the item at the given index. 19304 * @param index 19305 */ 19306 function select (index) { 19307 //-- force form to update state for validation 19308 $mdUtil.nextTick(function () { 19309 getDisplayValue(ctrl.matches[ index ]).then(function (val) { 19310 var ngModel = elements.$.input.controller('ngModel'); 19311 ngModel.$setViewValue(val); 19312 ngModel.$render(); 19313 }).finally(function () { 19314 $scope.selectedItem = ctrl.matches[ index ]; 19315 setLoading(false); 19316 }); 19317 }, false); 19318 } 19319 19320 /** 19321 * Clears the searchText value and selected item. 19322 */ 19323 function clearValue () { 19324 // Set the loading to true so we don't see flashes of content. 19325 // The flashing will only occur when an async request is running. 19326 // So the loading process will stop when the results had been retrieved. 19327 setLoading(true); 19328 19329 // Reset our variables 19330 ctrl.index = 0; 19331 ctrl.matches = []; 19332 $scope.searchText = ''; 19333 19334 // Per http://www.w3schools.com/jsref/event_oninput.asp 19335 var eventObj = document.createEvent('CustomEvent'); 19336 eventObj.initCustomEvent('input', true, true, { value: $scope.searchText }); 19337 elements.input.dispatchEvent(eventObj); 19338 19339 elements.input.focus(); 19340 } 19341 19342 /** 19343 * Fetches the results for the provided search text. 19344 * @param searchText 19345 */ 19346 function fetchResults (searchText) { 19347 var items = $scope.$parent.$eval(itemExpr), 19348 term = searchText.toLowerCase(), 19349 isList = angular.isArray(items), 19350 isPromise = !!items.then; // Every promise should contain a `then` property 19351 19352 if (isList) handleResults(items); 19353 else if (isPromise) handleAsyncResults(items); 19354 19355 function handleAsyncResults(items) { 19356 if ( !items ) return; 19357 19358 items = $q.when(items); 19359 fetchesInProgress++; 19360 setLoading(true); 19361 19362 $mdUtil.nextTick(function () { 19363 items 19364 .then(handleResults) 19365 .finally(function(){ 19366 if (--fetchesInProgress === 0) { 19367 setLoading(false); 19368 } 19369 }); 19370 },true, $scope); 19371 } 19372 19373 function handleResults (matches) { 19374 cache[ term ] = matches; 19375 if ((searchText || '') !== ($scope.searchText || '')) return; //-- just cache the results if old request 19376 19377 ctrl.matches = matches; 19378 ctrl.hidden = shouldHide(); 19379 19380 // If loading is in progress, then we'll end the progress. This is needed for example, 19381 // when the `clear` button was clicked, because there we always show the loading process, to prevent flashing. 19382 if (ctrl.loading) setLoading(false); 19383 19384 if ($scope.selectOnMatch) selectItemOnMatch(); 19385 19386 updateMessages(); 19387 positionDropdown(); 19388 } 19389 } 19390 19391 /** 19392 * Updates the ARIA messages 19393 */ 19394 function updateMessages () { 19395 getCurrentDisplayValue().then(function (msg) { 19396 ctrl.messages = [ getCountMessage(), msg ]; 19397 }); 19398 } 19399 19400 /** 19401 * Returns the ARIA message for how many results match the current query. 19402 * @returns {*} 19403 */ 19404 function getCountMessage () { 19405 if (lastCount === ctrl.matches.length) return ''; 19406 lastCount = ctrl.matches.length; 19407 switch (ctrl.matches.length) { 19408 case 0: 19409 return 'There are no matches available.'; 19410 case 1: 19411 return 'There is 1 match available.'; 19412 default: 19413 return 'There are ' + ctrl.matches.length + ' matches available.'; 19414 } 19415 } 19416 19417 /** 19418 * Makes sure that the focused element is within view. 19419 */ 19420 function updateScroll () { 19421 if (!elements.li[0]) return; 19422 var height = elements.li[0].offsetHeight, 19423 top = height * ctrl.index, 19424 bot = top + height, 19425 hgt = elements.scroller.clientHeight, 19426 scrollTop = elements.scroller.scrollTop; 19427 if (top < scrollTop) { 19428 scrollTo(top); 19429 } else if (bot > scrollTop + hgt) { 19430 scrollTo(bot - hgt); 19431 } 19432 } 19433 19434 function isPromiseFetching() { 19435 return fetchesInProgress !== 0; 19436 } 19437 19438 function scrollTo (offset) { 19439 elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset); 19440 } 19441 19442 function notFoundVisible () { 19443 var textLength = (ctrl.scope.searchText || '').length; 19444 19445 return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection(); 19446 } 19447 19448 /** 19449 * Starts the query to gather the results for the current searchText. Attempts to return cached 19450 * results first, then forwards the process to `fetchResults` if necessary. 19451 */ 19452 function handleQuery () { 19453 var searchText = $scope.searchText || '', 19454 term = searchText.toLowerCase(); 19455 //-- if results are cached, pull in cached results 19456 if (!$scope.noCache && cache[ term ]) { 19457 ctrl.matches = cache[ term ]; 19458 updateMessages(); 19459 } else { 19460 fetchResults(searchText); 19461 } 19462 19463 ctrl.hidden = shouldHide(); 19464 } 19465 19466 /** 19467 * If there is only one matching item and the search text matches its display value exactly, 19468 * automatically select that item. Note: This function is only called if the user uses the 19469 * `md-select-on-match` flag. 19470 */ 19471 function selectItemOnMatch () { 19472 var searchText = $scope.searchText, 19473 matches = ctrl.matches, 19474 item = matches[ 0 ]; 19475 if (matches.length === 1) getDisplayValue(item).then(function (displayValue) { 19476 var isMatching = searchText == displayValue; 19477 if ($scope.matchInsensitive && !isMatching) { 19478 isMatching = searchText.toLowerCase() == displayValue.toLowerCase(); 19479 } 19480 19481 if (isMatching) select(0); 19482 }); 19483 } 19484 19485 } 19486 MdAutocompleteCtrl.$inject = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q"]; 19487 19488 })(); 19489 (function(){ 19490 "use strict"; 19491 19492 angular 19493 .module('material.components.autocomplete') 19494 .directive('mdAutocomplete', MdAutocomplete); 19495 19496 /** 19497 * @ngdoc directive 19498 * @name mdAutocomplete 19499 * @module material.components.autocomplete 19500 * 19501 * @description 19502 * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a 19503 * custom query. This component allows you to provide real-time suggestions as the user types 19504 * in the input area. 19505 * 19506 * To start, you will need to specify the required parameters and provide a template for your 19507 * results. The content inside `md-autocomplete` will be treated as a template. 19508 * 19509 * In more complex cases, you may want to include other content such as a message to display when 19510 * no matches were found. You can do this by wrapping your template in `md-item-template` and 19511 * adding a tag for `md-not-found`. An example of this is shown below. 19512 * 19513 * ### Validation 19514 * 19515 * You can use `ng-messages` to include validation the same way that you would normally validate; 19516 * however, if you want to replicate a standard input with a floating label, you will have to 19517 * do the following: 19518 * 19519 * - Make sure that your template is wrapped in `md-item-template` 19520 * - Add your `ng-messages` code inside of `md-autocomplete` 19521 * - Add your validation properties to `md-autocomplete` (ie. `required`) 19522 * - Add a `name` to `md-autocomplete` (to be used on the generated `input`) 19523 * 19524 * There is an example below of how this should look. 19525 * 19526 * 19527 * @param {expression} md-items An expression in the format of `item in items` to iterate over 19528 * matches for your search. 19529 * @param {expression=} md-selected-item-change An expression to be run each time a new item is 19530 * selected 19531 * @param {expression=} md-search-text-change An expression to be run each time the search text 19532 * updates 19533 * @param {expression=} md-search-text A model to bind the search query text to 19534 * @param {object=} md-selected-item A model to bind the selected item to 19535 * @param {expression=} md-item-text An expression that will convert your object to a single string. 19536 * @param {string=} placeholder Placeholder text that will be forwarded to the input. 19537 * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete 19538 * @param {boolean=} ng-disabled Determines whether or not to disable the input field 19539 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will 19540 * make suggestions 19541 * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking 19542 * for results 19543 * @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a `$mdDialog`, 19544 * `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening. <br/><br/> 19545 * Also the autocomplete will immediately focus the input element. 19546 * @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating label 19547 * @param {boolean=} md-autoselect If true, the first item will be selected by default 19548 * @param {string=} md-menu-class This will be applied to the dropdown menu for styling 19549 * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in 19550 * `md-input-container` 19551 * @param {string=} md-input-name The name attribute given to the input element to be used with 19552 * FormController 19553 * @param {string=} md-select-on-focus When present the inputs text will be automatically selected 19554 * on focus. 19555 * @param {string=} md-input-id An ID to be added to the input element 19556 * @param {number=} md-input-minlength The minimum length for the input's value for validation 19557 * @param {number=} md-input-maxlength The maximum length for the input's value for validation 19558 * @param {boolean=} md-select-on-match When set, autocomplete will automatically select exact 19559 * the item if the search text is an exact match 19560 * @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete 19561 * will select on case-insensitive match 19562 * 19563 * @usage 19564 * ### Basic Example 19565 * <hljs lang="html"> 19566 * <md-autocomplete 19567 * md-selected-item="selectedItem" 19568 * md-search-text="searchText" 19569 * md-items="item in getMatches(searchText)" 19570 * md-item-text="item.display"> 19571 * <span md-highlight-text="searchText">{{item.display}}</span> 19572 * </md-autocomplete> 19573 * </hljs> 19574 * 19575 * ### Example with "not found" message 19576 * <hljs lang="html"> 19577 * <md-autocomplete 19578 * md-selected-item="selectedItem" 19579 * md-search-text="searchText" 19580 * md-items="item in getMatches(searchText)" 19581 * md-item-text="item.display"> 19582 * <md-item-template> 19583 * <span md-highlight-text="searchText">{{item.display}}</span> 19584 * </md-item-template> 19585 * <md-not-found> 19586 * No matches found. 19587 * </md-not-found> 19588 * </md-autocomplete> 19589 * </hljs> 19590 * 19591 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the 19592 * different parts that make up our component. 19593 * 19594 * ### Example with validation 19595 * <hljs lang="html"> 19596 * <form name="autocompleteForm"> 19597 * <md-autocomplete 19598 * required 19599 * md-input-name="autocomplete" 19600 * md-selected-item="selectedItem" 19601 * md-search-text="searchText" 19602 * md-items="item in getMatches(searchText)" 19603 * md-item-text="item.display"> 19604 * <md-item-template> 19605 * <span md-highlight-text="searchText">{{item.display}}</span> 19606 * </md-item-template> 19607 * <div ng-messages="autocompleteForm.autocomplete.$error"> 19608 * <div ng-message="required">This field is required</div> 19609 * </div> 19610 * </md-autocomplete> 19611 * </form> 19612 * </hljs> 19613 * 19614 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the 19615 * different parts that make up our component. 19616 */ 19617 19618 function MdAutocomplete () { 19619 19620 return { 19621 controller: 'MdAutocompleteCtrl', 19622 controllerAs: '$mdAutocompleteCtrl', 19623 scope: { 19624 inputName: '@mdInputName', 19625 inputMinlength: '@mdInputMinlength', 19626 inputMaxlength: '@mdInputMaxlength', 19627 searchText: '=?mdSearchText', 19628 selectedItem: '=?mdSelectedItem', 19629 itemsExpr: '@mdItems', 19630 itemText: '&mdItemText', 19631 placeholder: '@placeholder', 19632 noCache: '=?mdNoCache', 19633 selectOnMatch: '=?mdSelectOnMatch', 19634 matchInsensitive: '=?mdMatchCaseInsensitive', 19635 itemChange: '&?mdSelectedItemChange', 19636 textChange: '&?mdSearchTextChange', 19637 minLength: '=?mdMinLength', 19638 delay: '=?mdDelay', 19639 autofocus: '=?mdAutofocus', 19640 floatingLabel: '@?mdFloatingLabel', 19641 autoselect: '=?mdAutoselect', 19642 menuClass: '@?mdMenuClass', 19643 inputId: '@?mdInputId' 19644 }, 19645 link: function(scope, element, attrs, controller) { 19646 // Retrieve the state of using a md-not-found template by using our attribute, which will 19647 // be added to the element in the template function. 19648 controller.hasNotFound = !!element.attr('md-has-not-found'); 19649 }, 19650 template: function (element, attr) { 19651 var noItemsTemplate = getNoItemsTemplate(), 19652 itemTemplate = getItemTemplate(), 19653 leftover = element.html(), 19654 tabindex = attr.tabindex; 19655 19656 // Set our attribute for the link function above which runs later. 19657 // We will set an attribute, because otherwise the stored variables will be trashed when 19658 // removing the element is hidden while retrieving the template. For example when using ngIf. 19659 if (noItemsTemplate) element.attr('md-has-not-found', true); 19660 19661 // Always set our tabindex of the autocomplete directive to -1, because our input 19662 // will hold the actual tabindex. 19663 element.attr('tabindex', '-1'); 19664 19665 return '\ 19666 <md-autocomplete-wrap\ 19667 layout="row"\ 19668 ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \'md-menu-showing\': !$mdAutocompleteCtrl.hidden }"\ 19669 role="listbox">\ 19670 ' + getInputElement() + '\ 19671 <md-progress-linear\ 19672 class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\ 19673 ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\ 19674 md-mode="indeterminate"></md-progress-linear>\ 19675 <md-virtual-repeat-container\ 19676 md-auto-shrink\ 19677 md-auto-shrink-min="1"\ 19678 ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\ 19679 ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\ 19680 ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\ 19681 ng-hide="$mdAutocompleteCtrl.hidden"\ 19682 class="md-autocomplete-suggestions-container md-whiteframe-z1"\ 19683 ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\ 19684 role="presentation">\ 19685 <ul class="md-autocomplete-suggestions"\ 19686 ng-class="::menuClass"\ 19687 id="ul-{{$mdAutocompleteCtrl.id}}">\ 19688 <li md-virtual-repeat="item in $mdAutocompleteCtrl.matches"\ 19689 ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\ 19690 ng-click="$mdAutocompleteCtrl.select($index)"\ 19691 md-extra-name="$mdAutocompleteCtrl.itemName">\ 19692 ' + itemTemplate + '\ 19693 </li>' + noItemsTemplate + '\ 19694 </ul>\ 19695 </md-virtual-repeat-container>\ 19696 </md-autocomplete-wrap>\ 19697 <aria-status\ 19698 class="md-visually-hidden"\ 19699 role="status"\ 19700 aria-live="assertive">\ 19701 <p ng-repeat="message in $mdAutocompleteCtrl.messages track by $index" ng-if="message">{{message}}</p>\ 19702 </aria-status>'; 19703 19704 function getItemTemplate() { 19705 var templateTag = element.find('md-item-template').detach(), 19706 html = templateTag.length ? templateTag.html() : element.html(); 19707 if (!templateTag.length) element.empty(); 19708 return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html + '</md-autocomplete-parent-scope>'; 19709 } 19710 19711 function getNoItemsTemplate() { 19712 var templateTag = element.find('md-not-found').detach(), 19713 template = templateTag.length ? templateTag.html() : ''; 19714 return template 19715 ? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()"\ 19716 md-autocomplete-parent-scope>' + template + '</li>' 19717 : ''; 19718 19719 } 19720 19721 function getInputElement () { 19722 if (attr.mdFloatingLabel) { 19723 return '\ 19724 <md-input-container flex ng-if="floatingLabel">\ 19725 <label>{{floatingLabel}}</label>\ 19726 <input type="search"\ 19727 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\ 19728 id="{{ inputId || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\ 19729 name="{{inputName}}"\ 19730 autocomplete="off"\ 19731 ng-required="$mdAutocompleteCtrl.isRequired"\ 19732 ng-readonly="$mdAutocompleteCtrl.isReadonly"\ 19733 ng-minlength="inputMinlength"\ 19734 ng-maxlength="inputMaxlength"\ 19735 ng-disabled="$mdAutocompleteCtrl.isDisabled"\ 19736 ng-model="$mdAutocompleteCtrl.scope.searchText"\ 19737 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\ 19738 ng-blur="$mdAutocompleteCtrl.blur()"\ 19739 ' + (attr.mdNoAsterisk != null ? 'md-no-asterisk="' + attr.mdNoAsterisk + '"' : '') + '\ 19740 ng-focus="$mdAutocompleteCtrl.focus()"\ 19741 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\ 19742 ' + (attr.mdSelectOnFocus != null ? 'md-select-on-focus=""' : '') + '\ 19743 aria-label="{{floatingLabel}}"\ 19744 aria-autocomplete="list"\ 19745 aria-haspopup="true"\ 19746 aria-activedescendant=""\ 19747 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\ 19748 <div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\ 19749 </md-input-container>'; 19750 } else { 19751 return '\ 19752 <input flex type="search"\ 19753 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\ 19754 id="{{ inputId || \'input-\' + $mdAutocompleteCtrl.id }}"\ 19755 name="{{inputName}}"\ 19756 ng-if="!floatingLabel"\ 19757 autocomplete="off"\ 19758 ng-required="$mdAutocompleteCtrl.isRequired"\ 19759 ng-disabled="$mdAutocompleteCtrl.isDisabled"\ 19760 ng-readonly="$mdAutocompleteCtrl.isReadonly"\ 19761 ng-model="$mdAutocompleteCtrl.scope.searchText"\ 19762 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\ 19763 ng-blur="$mdAutocompleteCtrl.blur()"\ 19764 ng-focus="$mdAutocompleteCtrl.focus()"\ 19765 placeholder="{{placeholder}}"\ 19766 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\ 19767 ' + (attr.mdSelectOnFocus != null ? 'md-select-on-focus=""' : '') + '\ 19768 aria-label="{{placeholder}}"\ 19769 aria-autocomplete="list"\ 19770 aria-haspopup="true"\ 19771 aria-activedescendant=""\ 19772 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\ 19773 <button\ 19774 type="button"\ 19775 tabindex="-1"\ 19776 ng-if="$mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled"\ 19777 ng-click="$mdAutocompleteCtrl.clear()">\ 19778 <md-icon md-svg-icon="md-close"></md-icon>\ 19779 <span class="md-visually-hidden">Clear</span>\ 19780 </button>\ 19781 '; 19782 } 19783 } 19784 } 19785 }; 19786 } 19787 19788 })(); 19789 (function(){ 19790 "use strict"; 19791 19792 angular 19793 .module('material.components.autocomplete') 19794 .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective); 19795 19796 function MdAutocompleteItemScopeDirective($compile, $mdUtil) { 19797 return { 19798 restrict: 'AE', 19799 compile: compile, 19800 terminal: true, 19801 transclude: 'element' 19802 }; 19803 19804 function compile(tElement, tAttr, transclude) { 19805 return function postLink(scope, element, attr) { 19806 var ctrl = scope.$mdAutocompleteCtrl; 19807 var newScope = ctrl.parent.$new(); 19808 var itemName = ctrl.itemName; 19809 19810 // Watch for changes to our scope's variables and copy them to the new scope 19811 watchVariable('$index', '$index'); 19812 watchVariable('item', itemName); 19813 19814 // Ensure that $digest calls on our scope trigger $digest on newScope. 19815 connectScopes(); 19816 19817 // Link the element against newScope. 19818 transclude(newScope, function(clone) { 19819 element.after(clone); 19820 }); 19821 19822 /** 19823 * Creates a watcher for variables that are copied from the parent scope 19824 * @param variable 19825 * @param alias 19826 */ 19827 function watchVariable(variable, alias) { 19828 newScope[alias] = scope[variable]; 19829 19830 scope.$watch(variable, function(value) { 19831 $mdUtil.nextTick(function() { 19832 newScope[alias] = value; 19833 }); 19834 }); 19835 } 19836 19837 /** 19838 * Creates watchers on scope and newScope that ensure that for any 19839 * $digest of scope, newScope is also $digested. 19840 */ 19841 function connectScopes() { 19842 var scopeDigesting = false; 19843 var newScopeDigesting = false; 19844 19845 scope.$watch(function() { 19846 if (newScopeDigesting || scopeDigesting) { 19847 return; 19848 } 19849 19850 scopeDigesting = true; 19851 scope.$$postDigest(function() { 19852 if (!newScopeDigesting) { 19853 newScope.$digest(); 19854 } 19855 19856 scopeDigesting = newScopeDigesting = false; 19857 }); 19858 }); 19859 19860 newScope.$watch(function() { 19861 newScopeDigesting = true; 19862 }); 19863 } 19864 }; 19865 } 19866 } 19867 MdAutocompleteItemScopeDirective.$inject = ["$compile", "$mdUtil"]; 19868 })(); 19869 (function(){ 19870 "use strict"; 19871 19872 angular 19873 .module('material.components.autocomplete') 19874 .controller('MdHighlightCtrl', MdHighlightCtrl); 19875 19876 function MdHighlightCtrl ($scope, $element, $attrs) { 19877 this.init = init; 19878 19879 function init (termExpr, unsafeTextExpr) { 19880 var text = null, 19881 regex = null, 19882 flags = $attrs.mdHighlightFlags || '', 19883 watcher = $scope.$watch(function($scope) { 19884 return { 19885 term: termExpr($scope), 19886 unsafeText: unsafeTextExpr($scope) 19887 }; 19888 }, function (state, prevState) { 19889 if (text === null || state.unsafeText !== prevState.unsafeText) { 19890 text = angular.element('<div>').text(state.unsafeText).html() 19891 } 19892 if (regex === null || state.term !== prevState.term) { 19893 regex = getRegExp(state.term, flags); 19894 } 19895 19896 $element.html(text.replace(regex, '<span class="highlight">$&</span>')); 19897 }, true); 19898 $element.on('$destroy', watcher); 19899 } 19900 19901 function sanitize (term) { 19902 return term && term.replace(/[\\\^\$\*\+\?\.\(\)\|\{}\[\]]/g, '\\$&'); 19903 } 19904 19905 function getRegExp (text, flags) { 19906 var str = ''; 19907 if (flags.indexOf('^') >= 1) str += '^'; 19908 str += text; 19909 if (flags.indexOf('$') >= 1) str += '$'; 19910 return new RegExp(sanitize(str), flags.replace(/[\$\^]/g, '')); 19911 } 19912 } 19913 MdHighlightCtrl.$inject = ["$scope", "$element", "$attrs"]; 19914 19915 })(); 19916 (function(){ 19917 "use strict"; 19918 19919 angular 19920 .module('material.components.autocomplete') 19921 .directive('mdHighlightText', MdHighlight); 19922 19923 /** 19924 * @ngdoc directive 19925 * @name mdHighlightText 19926 * @module material.components.autocomplete 19927 * 19928 * @description 19929 * The `md-highlight-text` directive allows you to specify text that should be highlighted within 19930 * an element. Highlighted text will be wrapped in `<span class="highlight"></span>` which can 19931 * be styled through CSS. Please note that child elements may not be used with this directive. 19932 * 19933 * @param {string} md-highlight-text A model to be searched for 19934 * @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags). 19935 * #### **Supported flags**: 19936 * - `g`: Find all matches within the provided text 19937 * - `i`: Ignore case when searching for matches 19938 * - `$`: Only match if the text ends with the search term 19939 * - `^`: Only match if the text begins with the search term 19940 * 19941 * @usage 19942 * <hljs lang="html"> 19943 * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" /> 19944 * <ul> 19945 * <li ng-repeat="result in results" md-highlight-text="searchTerm"> 19946 * {{result.text}} 19947 * </li> 19948 * </ul> 19949 * </hljs> 19950 */ 19951 19952 function MdHighlight ($interpolate, $parse) { 19953 return { 19954 terminal: true, 19955 controller: 'MdHighlightCtrl', 19956 compile: function mdHighlightCompile(tElement, tAttr) { 19957 var termExpr = $parse(tAttr.mdHighlightText); 19958 var unsafeTextExpr = $interpolate(tElement.html()); 19959 19960 return function mdHighlightLink(scope, element, attr, ctrl) { 19961 ctrl.init(termExpr, unsafeTextExpr); 19962 }; 19963 } 19964 }; 19965 } 19966 MdHighlight.$inject = ["$interpolate", "$parse"]; 19967 19968 })(); 19969 (function(){ 19970 "use strict"; 19971 19972 angular 19973 .module('material.components.chips') 19974 .directive('mdChip', MdChip); 19975 19976 /** 19977 * @ngdoc directive 19978 * @name mdChip 19979 * @module material.components.chips 19980 * 19981 * @description 19982 * `<md-chip>` is a component used within `<md-chips>` and is responsible for rendering individual 19983 * chips. 19984 * 19985 * 19986 * @usage 19987 * <hljs lang="html"> 19988 * <md-chip>{{$chip}}</md-chip> 19989 * </hljs> 19990 * 19991 */ 19992 19993 // This hint text is hidden within a chip but used by screen readers to 19994 // inform the user how they can interact with a chip. 19995 var DELETE_HINT_TEMPLATE = '\ 19996 <span ng-if="!$mdChipsCtrl.readonly" class="md-visually-hidden">\ 19997 {{$mdChipsCtrl.deleteHint}}\ 19998 </span>'; 19999 20000 /** 20001 * MDChip Directive Definition 20002 * 20003 * @param $mdTheming 20004 * @param $mdInkRipple 20005 * @ngInject 20006 */ 20007 function MdChip($mdTheming, $mdUtil) { 20008 var hintTemplate = $mdUtil.processTemplate(DELETE_HINT_TEMPLATE); 20009 20010 return { 20011 restrict: 'E', 20012 require: '^?mdChips', 20013 compile: compile 20014 }; 20015 20016 function compile(element, attr) { 20017 // Append the delete template 20018 element.append($mdUtil.processTemplate(hintTemplate)); 20019 20020 return function postLink(scope, element, attr, ctrl) { 20021 element.addClass('md-chip'); 20022 $mdTheming(element); 20023 20024 if (ctrl) angular.element(element[0].querySelector('.md-chip-content')) 20025 .on('blur', function () { 20026 ctrl.resetSelectedChip(); 20027 ctrl.$scope.$applyAsync(); 20028 }); 20029 }; 20030 } 20031 } 20032 MdChip.$inject = ["$mdTheming", "$mdUtil"]; 20033 20034 })(); 20035 (function(){ 20036 "use strict"; 20037 20038 angular 20039 .module('material.components.chips') 20040 .directive('mdChipRemove', MdChipRemove); 20041 20042 /** 20043 * @ngdoc directive 20044 * @name mdChipRemove 20045 * @module material.components.chips 20046 * 20047 * @description 20048 * `<md-chip-remove>` 20049 * Designates an element to be used as the delete button for a chip. This 20050 * element is passed as a child of the `md-chips` element. 20051 * 20052 * @usage 20053 * <hljs lang="html"> 20054 * <md-chips><button md-chip-remove>DEL</button></md-chips> 20055 * </hljs> 20056 */ 20057 20058 20059 /** 20060 * MdChipRemove Directive Definition. 20061 * 20062 * @param $compile 20063 * @param $timeout 20064 * @returns {{restrict: string, require: string[], link: Function, scope: boolean}} 20065 * @constructor 20066 */ 20067 function MdChipRemove ($timeout) { 20068 return { 20069 restrict: 'A', 20070 require: '^mdChips', 20071 scope: false, 20072 link: postLink 20073 }; 20074 20075 function postLink(scope, element, attr, ctrl) { 20076 element.on('click', function(event) { 20077 scope.$apply(function() { 20078 ctrl.removeChip(scope.$$replacedScope.$index); 20079 }); 20080 }); 20081 20082 // Child elements aren't available until after a $timeout tick as they are hidden by an 20083 // `ng-if`. see http://goo.gl/zIWfuw 20084 $timeout(function() { 20085 element.attr({ tabindex: -1, 'aria-hidden': true }); 20086 element.find('button').attr('tabindex', '-1'); 20087 }); 20088 } 20089 } 20090 MdChipRemove.$inject = ["$timeout"]; 20091 20092 })(); 20093 (function(){ 20094 "use strict"; 20095 20096 angular 20097 .module('material.components.chips') 20098 .directive('mdChipTransclude', MdChipTransclude); 20099 20100 function MdChipTransclude ($compile) { 20101 return { 20102 restrict: 'EA', 20103 terminal: true, 20104 link: link, 20105 scope: false 20106 }; 20107 function link (scope, element, attr) { 20108 var ctrl = scope.$parent.$mdChipsCtrl, 20109 newScope = ctrl.parent.$new(false, ctrl.parent); 20110 newScope.$$replacedScope = scope; 20111 newScope.$chip = scope.$chip; 20112 newScope.$index = scope.$index; 20113 newScope.$mdChipsCtrl = ctrl; 20114 20115 var newHtml = ctrl.$scope.$eval(attr.mdChipTransclude); 20116 20117 element.html(newHtml); 20118 $compile(element.contents())(newScope); 20119 } 20120 } 20121 MdChipTransclude.$inject = ["$compile"]; 20122 20123 })(); 20124 (function(){ 20125 "use strict"; 20126 20127 angular 20128 .module('material.components.chips') 20129 .controller('MdChipsCtrl', MdChipsCtrl); 20130 20131 /** 20132 * Controller for the MdChips component. Responsible for adding to and 20133 * removing from the list of chips, marking chips as selected, and binding to 20134 * the models of various input components. 20135 * 20136 * @param $scope 20137 * @param $mdConstant 20138 * @param $log 20139 * @param $element 20140 * @param $mdUtil 20141 * @constructor 20142 */ 20143 function MdChipsCtrl ($scope, $mdConstant, $log, $element, $timeout, $mdUtil) { 20144 /** @type {$timeout} **/ 20145 this.$timeout = $timeout; 20146 20147 /** @type {Object} */ 20148 this.$mdConstant = $mdConstant; 20149 20150 /** @type {angular.$scope} */ 20151 this.$scope = $scope; 20152 20153 /** @type {angular.$scope} */ 20154 this.parent = $scope.$parent; 20155 20156 /** @type {$log} */ 20157 this.$log = $log; 20158 20159 /** @type {$element} */ 20160 this.$element = $element; 20161 20162 /** @type {angular.NgModelController} */ 20163 this.ngModelCtrl = null; 20164 20165 /** @type {angular.NgModelController} */ 20166 this.userInputNgModelCtrl = null; 20167 20168 /** @type {Element} */ 20169 this.userInputElement = null; 20170 20171 /** @type {Array.<Object>} */ 20172 this.items = []; 20173 20174 /** @type {number} */ 20175 this.selectedChip = -1; 20176 20177 /** @type {boolean} */ 20178 this.hasAutocomplete = false; 20179 20180 /** @type {string} */ 20181 this.enableChipEdit = $mdUtil.parseAttributeBoolean(this.mdEnableChipEdit); 20182 20183 /** 20184 * Hidden hint text for how to delete a chip. Used to give context to screen readers. 20185 * @type {string} 20186 */ 20187 this.deleteHint = 'Press delete to remove this chip.'; 20188 20189 /** 20190 * Hidden label for the delete button. Used to give context to screen readers. 20191 * @type {string} 20192 */ 20193 this.deleteButtonLabel = 'Remove'; 20194 20195 /** 20196 * Model used by the input element. 20197 * @type {string} 20198 */ 20199 this.chipBuffer = ''; 20200 20201 /** 20202 * Whether to use the onAppend expression to transform the chip buffer 20203 * before appending it to the list. 20204 * @type {boolean} 20205 * 20206 * 20207 * @deprecated Will remove in 1.0. 20208 */ 20209 this.useOnAppend = false; 20210 20211 /** 20212 * Whether to use the transformChip expression to transform the chip buffer 20213 * before appending it to the list. 20214 * @type {boolean} 20215 */ 20216 this.useTransformChip = false; 20217 20218 /** 20219 * Whether to use the onAdd expression to notify of chip additions. 20220 * @type {boolean} 20221 */ 20222 this.useOnAdd = false; 20223 20224 /** 20225 * Whether to use the onRemove expression to notify of chip removals. 20226 * @type {boolean} 20227 */ 20228 this.useOnRemove = false; 20229 20230 /** 20231 * Whether to use the onSelect expression to notify the component's user 20232 * after selecting a chip from the list. 20233 * @type {boolean} 20234 */ 20235 this.useOnSelect = false; 20236 } 20237 MdChipsCtrl.$inject = ["$scope", "$mdConstant", "$log", "$element", "$timeout", "$mdUtil"]; 20238 20239 /** 20240 * Handles the keydown event on the input element: by default <enter> appends 20241 * the buffer to the chip list, while backspace removes the last chip in the 20242 * list if the current buffer is empty. 20243 * @param event 20244 */ 20245 MdChipsCtrl.prototype.inputKeydown = function(event) { 20246 var chipBuffer = this.getChipBuffer(); 20247 20248 // If we have an autocomplete, and it handled the event, we have nothing to do 20249 if (this.hasAutocomplete && event.isDefaultPrevented && event.isDefaultPrevented()) { 20250 return; 20251 } 20252 20253 if (event.keyCode === this.$mdConstant.KEY_CODE.BACKSPACE) { 20254 if (chipBuffer) return; 20255 event.preventDefault(); 20256 event.stopPropagation(); 20257 if (this.items.length) this.selectAndFocusChipSafe(this.items.length - 1); 20258 return; 20259 } 20260 20261 // By default <enter> appends the buffer to the chip list. 20262 if (!this.separatorKeys || this.separatorKeys.length < 1) { 20263 this.separatorKeys = [this.$mdConstant.KEY_CODE.ENTER]; 20264 } 20265 20266 // Support additional separator key codes in an array of `md-separator-keys`. 20267 if (this.separatorKeys.indexOf(event.keyCode) !== -1) { 20268 if ((this.hasAutocomplete && this.requireMatch) || !chipBuffer) return; 20269 event.preventDefault(); 20270 20271 // Only append the chip and reset the chip buffer if the max chips limit isn't reached. 20272 if (this.hasMaxChipsReached()) return; 20273 20274 this.appendChip(chipBuffer.trim()); 20275 this.resetChipBuffer(); 20276 } 20277 }; 20278 20279 20280 /** 20281 * Updates the content of the chip at given index 20282 * @param chipIndex 20283 * @param chipContents 20284 */ 20285 MdChipsCtrl.prototype.updateChipContents = function(chipIndex, chipContents){ 20286 if(chipIndex >= 0 && chipIndex < this.items.length) { 20287 this.items[chipIndex] = chipContents; 20288 this.ngModelCtrl.$setDirty(); 20289 } 20290 }; 20291 20292 20293 /** 20294 * Returns true if a chip is currently being edited. False otherwise. 20295 * @return {boolean} 20296 */ 20297 MdChipsCtrl.prototype.isEditingChip = function(){ 20298 return !!this.$element[0].getElementsByClassName('_md-chip-editing').length; 20299 }; 20300 20301 20302 /** 20303 * Handles the keydown event on the chip elements: backspace removes the selected chip, arrow 20304 * keys switch which chips is active 20305 * @param event 20306 */ 20307 MdChipsCtrl.prototype.chipKeydown = function (event) { 20308 if (this.getChipBuffer()) return; 20309 if (this.isEditingChip()) return; 20310 20311 switch (event.keyCode) { 20312 case this.$mdConstant.KEY_CODE.BACKSPACE: 20313 case this.$mdConstant.KEY_CODE.DELETE: 20314 if (this.selectedChip < 0) return; 20315 event.preventDefault(); 20316 this.removeAndSelectAdjacentChip(this.selectedChip); 20317 break; 20318 case this.$mdConstant.KEY_CODE.LEFT_ARROW: 20319 event.preventDefault(); 20320 if (this.selectedChip < 0) this.selectedChip = this.items.length; 20321 if (this.items.length) this.selectAndFocusChipSafe(this.selectedChip - 1); 20322 break; 20323 case this.$mdConstant.KEY_CODE.RIGHT_ARROW: 20324 event.preventDefault(); 20325 this.selectAndFocusChipSafe(this.selectedChip + 1); 20326 break; 20327 case this.$mdConstant.KEY_CODE.ESCAPE: 20328 case this.$mdConstant.KEY_CODE.TAB: 20329 if (this.selectedChip < 0) return; 20330 event.preventDefault(); 20331 this.onFocus(); 20332 break; 20333 } 20334 }; 20335 20336 /** 20337 * Get the input's placeholder - uses `placeholder` when list is empty and `secondary-placeholder` 20338 * when the list is non-empty. If `secondary-placeholder` is not provided, `placeholder` is used 20339 * always. 20340 */ 20341 MdChipsCtrl.prototype.getPlaceholder = function() { 20342 // Allow `secondary-placeholder` to be blank. 20343 var useSecondary = (this.items.length && 20344 (this.secondaryPlaceholder == '' || this.secondaryPlaceholder)); 20345 return useSecondary ? this.secondaryPlaceholder : this.placeholder; 20346 }; 20347 20348 /** 20349 * Removes chip at {@code index} and selects the adjacent chip. 20350 * @param index 20351 */ 20352 MdChipsCtrl.prototype.removeAndSelectAdjacentChip = function(index) { 20353 var selIndex = this.getAdjacentChipIndex(index); 20354 this.removeChip(index); 20355 this.$timeout(angular.bind(this, function () { 20356 this.selectAndFocusChipSafe(selIndex); 20357 })); 20358 }; 20359 20360 /** 20361 * Sets the selected chip index to -1. 20362 */ 20363 MdChipsCtrl.prototype.resetSelectedChip = function() { 20364 this.selectedChip = -1; 20365 }; 20366 20367 /** 20368 * Gets the index of an adjacent chip to select after deletion. Adjacency is 20369 * determined as the next chip in the list, unless the target chip is the 20370 * last in the list, then it is the chip immediately preceding the target. If 20371 * there is only one item in the list, -1 is returned (select none). 20372 * The number returned is the index to select AFTER the target has been 20373 * removed. 20374 * If the current chip is not selected, then -1 is returned to select none. 20375 */ 20376 MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) { 20377 var len = this.items.length - 1; 20378 return (len == 0) ? -1 : 20379 (index == len) ? index -1 : index; 20380 }; 20381 20382 /** 20383 * Append the contents of the buffer to the chip list. This method will first 20384 * call out to the md-transform-chip method, if provided. 20385 * 20386 * @param newChip 20387 */ 20388 MdChipsCtrl.prototype.appendChip = function(newChip) { 20389 if (this.useTransformChip && this.transformChip) { 20390 var transformedChip = this.transformChip({'$chip': newChip}); 20391 20392 // Check to make sure the chip is defined before assigning it, otherwise, we'll just assume 20393 // they want the string version. 20394 if (angular.isDefined(transformedChip)) { 20395 newChip = transformedChip; 20396 } 20397 } 20398 20399 // If items contains an identical object to newChip, do not append 20400 if (angular.isObject(newChip)){ 20401 var identical = this.items.some(function(item){ 20402 return angular.equals(newChip, item); 20403 }); 20404 if (identical) return; 20405 } 20406 20407 // Check for a null (but not undefined), or existing chip and cancel appending 20408 if (newChip == null || this.items.indexOf(newChip) + 1) return; 20409 20410 // Append the new chip onto our list 20411 var index = this.items.push(newChip); 20412 20413 // Update model validation 20414 this.ngModelCtrl.$setDirty(); 20415 this.validateModel(); 20416 20417 // If they provide the md-on-add attribute, notify them of the chip addition 20418 if (this.useOnAdd && this.onAdd) { 20419 this.onAdd({ '$chip': newChip, '$index': index }); 20420 } 20421 }; 20422 20423 /** 20424 * Sets whether to use the md-on-append expression. This expression is 20425 * bound to scope and controller in {@code MdChipsDirective} as 20426 * {@code onAppend}. Due to the nature of directive scope bindings, the 20427 * controller cannot know on its own/from the scope whether an expression was 20428 * actually provided. 20429 * 20430 * @deprecated 20431 * 20432 * TODO: Remove deprecated md-on-append functionality in 1.0 20433 */ 20434 MdChipsCtrl.prototype.useOnAppendExpression = function() { 20435 this.$log.warn("md-on-append is deprecated; please use md-transform-chip or md-on-add instead"); 20436 if (!this.useTransformChip || !this.transformChip) { 20437 this.useTransformChip = true; 20438 this.transformChip = this.onAppend; 20439 } 20440 }; 20441 20442 /** 20443 * Sets whether to use the md-transform-chip expression. This expression is 20444 * bound to scope and controller in {@code MdChipsDirective} as 20445 * {@code transformChip}. Due to the nature of directive scope bindings, the 20446 * controller cannot know on its own/from the scope whether an expression was 20447 * actually provided. 20448 */ 20449 MdChipsCtrl.prototype.useTransformChipExpression = function() { 20450 this.useTransformChip = true; 20451 }; 20452 20453 /** 20454 * Sets whether to use the md-on-add expression. This expression is 20455 * bound to scope and controller in {@code MdChipsDirective} as 20456 * {@code onAdd}. Due to the nature of directive scope bindings, the 20457 * controller cannot know on its own/from the scope whether an expression was 20458 * actually provided. 20459 */ 20460 MdChipsCtrl.prototype.useOnAddExpression = function() { 20461 this.useOnAdd = true; 20462 }; 20463 20464 /** 20465 * Sets whether to use the md-on-remove expression. This expression is 20466 * bound to scope and controller in {@code MdChipsDirective} as 20467 * {@code onRemove}. Due to the nature of directive scope bindings, the 20468 * controller cannot know on its own/from the scope whether an expression was 20469 * actually provided. 20470 */ 20471 MdChipsCtrl.prototype.useOnRemoveExpression = function() { 20472 this.useOnRemove = true; 20473 }; 20474 20475 /* 20476 * Sets whether to use the md-on-select expression. This expression is 20477 * bound to scope and controller in {@code MdChipsDirective} as 20478 * {@code onSelect}. Due to the nature of directive scope bindings, the 20479 * controller cannot know on its own/from the scope whether an expression was 20480 * actually provided. 20481 */ 20482 MdChipsCtrl.prototype.useOnSelectExpression = function() { 20483 this.useOnSelect = true; 20484 }; 20485 20486 /** 20487 * Gets the input buffer. The input buffer can be the model bound to the 20488 * default input item {@code this.chipBuffer}, the {@code selectedItem} 20489 * model of an {@code md-autocomplete}, or, through some magic, the model 20490 * bound to any inpput or text area element found within a 20491 * {@code md-input-container} element. 20492 * @return {Object|string} 20493 */ 20494 MdChipsCtrl.prototype.getChipBuffer = function() { 20495 return !this.userInputElement ? this.chipBuffer : 20496 this.userInputNgModelCtrl ? this.userInputNgModelCtrl.$viewValue : 20497 this.userInputElement[0].value; 20498 }; 20499 20500 /** 20501 * Resets the input buffer for either the internal input or user provided input element. 20502 */ 20503 MdChipsCtrl.prototype.resetChipBuffer = function() { 20504 if (this.userInputElement) { 20505 if (this.userInputNgModelCtrl) { 20506 this.userInputNgModelCtrl.$setViewValue(''); 20507 this.userInputNgModelCtrl.$render(); 20508 } else { 20509 this.userInputElement[0].value = ''; 20510 } 20511 } else { 20512 this.chipBuffer = ''; 20513 } 20514 }; 20515 20516 MdChipsCtrl.prototype.hasMaxChipsReached = function() { 20517 if (angular.isString(this.maxChips)) this.maxChips = parseInt(this.maxChips, 10) || 0; 20518 20519 return this.maxChips > 0 && this.items.length >= this.maxChips; 20520 }; 20521 20522 /** 20523 * Updates the validity properties for the ngModel. 20524 */ 20525 MdChipsCtrl.prototype.validateModel = function() { 20526 this.ngModelCtrl.$setValidity('md-max-chips', !this.hasMaxChipsReached()); 20527 }; 20528 20529 /** 20530 * Removes the chip at the given index. 20531 * @param index 20532 */ 20533 MdChipsCtrl.prototype.removeChip = function(index) { 20534 var removed = this.items.splice(index, 1); 20535 20536 // Update model validation 20537 this.ngModelCtrl.$setDirty(); 20538 this.validateModel(); 20539 20540 if (removed && removed.length && this.useOnRemove && this.onRemove) { 20541 this.onRemove({ '$chip': removed[0], '$index': index }); 20542 } 20543 }; 20544 20545 MdChipsCtrl.prototype.removeChipAndFocusInput = function (index) { 20546 this.removeChip(index); 20547 this.onFocus(); 20548 }; 20549 /** 20550 * Selects the chip at `index`, 20551 * @param index 20552 */ 20553 MdChipsCtrl.prototype.selectAndFocusChipSafe = function(index) { 20554 if (!this.items.length) { 20555 this.selectChip(-1); 20556 this.onFocus(); 20557 return; 20558 } 20559 if (index === this.items.length) return this.onFocus(); 20560 index = Math.max(index, 0); 20561 index = Math.min(index, this.items.length - 1); 20562 this.selectChip(index); 20563 this.focusChip(index); 20564 }; 20565 20566 /** 20567 * Marks the chip at the given index as selected. 20568 * @param index 20569 */ 20570 MdChipsCtrl.prototype.selectChip = function(index) { 20571 if (index >= -1 && index <= this.items.length) { 20572 this.selectedChip = index; 20573 20574 // Fire the onSelect if provided 20575 if (this.useOnSelect && this.onSelect) { 20576 this.onSelect({'$chip': this.items[this.selectedChip] }); 20577 } 20578 } else { 20579 this.$log.warn('Selected Chip index out of bounds; ignoring.'); 20580 } 20581 }; 20582 20583 /** 20584 * Selects the chip at `index` and gives it focus. 20585 * @param index 20586 */ 20587 MdChipsCtrl.prototype.selectAndFocusChip = function(index) { 20588 this.selectChip(index); 20589 if (index != -1) { 20590 this.focusChip(index); 20591 } 20592 }; 20593 20594 /** 20595 * Call `focus()` on the chip at `index` 20596 */ 20597 MdChipsCtrl.prototype.focusChip = function(index) { 20598 this.$element[0].querySelector('md-chip[index="' + index + '"] .md-chip-content').focus(); 20599 }; 20600 20601 /** 20602 * Configures the required interactions with the ngModel Controller. 20603 * Specifically, set {@code this.items} to the {@code NgModelCtrl#$viewVale}. 20604 * @param ngModelCtrl 20605 */ 20606 MdChipsCtrl.prototype.configureNgModel = function(ngModelCtrl) { 20607 this.ngModelCtrl = ngModelCtrl; 20608 20609 var self = this; 20610 ngModelCtrl.$render = function() { 20611 // model is updated. do something. 20612 self.items = self.ngModelCtrl.$viewValue; 20613 }; 20614 }; 20615 20616 MdChipsCtrl.prototype.onFocus = function () { 20617 var input = this.$element[0].querySelector('input'); 20618 input && input.focus(); 20619 this.resetSelectedChip(); 20620 }; 20621 20622 MdChipsCtrl.prototype.onInputFocus = function () { 20623 this.inputHasFocus = true; 20624 this.resetSelectedChip(); 20625 }; 20626 20627 MdChipsCtrl.prototype.onInputBlur = function () { 20628 this.inputHasFocus = false; 20629 }; 20630 20631 /** 20632 * Configure event bindings on a user-provided input element. 20633 * @param inputElement 20634 */ 20635 MdChipsCtrl.prototype.configureUserInput = function(inputElement) { 20636 this.userInputElement = inputElement; 20637 20638 // Find the NgModelCtrl for the input element 20639 var ngModelCtrl = inputElement.controller('ngModel'); 20640 // `.controller` will look in the parent as well. 20641 if (ngModelCtrl != this.ngModelCtrl) { 20642 this.userInputNgModelCtrl = ngModelCtrl; 20643 } 20644 20645 var scope = this.$scope; 20646 var ctrl = this; 20647 20648 // Run all of the events using evalAsync because a focus may fire a blur in the same digest loop 20649 var scopeApplyFn = function(event, fn) { 20650 scope.$evalAsync(angular.bind(ctrl, fn, event)); 20651 }; 20652 20653 // Bind to keydown and focus events of input 20654 inputElement 20655 .attr({ tabindex: 0 }) 20656 .on('keydown', function(event) { scopeApplyFn(event, ctrl.inputKeydown) }) 20657 .on('focus', function(event) { scopeApplyFn(event, ctrl.onInputFocus) }) 20658 .on('blur', function(event) { scopeApplyFn(event, ctrl.onInputBlur) }) 20659 }; 20660 20661 MdChipsCtrl.prototype.configureAutocomplete = function(ctrl) { 20662 if ( ctrl ) { 20663 this.hasAutocomplete = true; 20664 20665 ctrl.registerSelectedItemWatcher(angular.bind(this, function (item) { 20666 if (item) { 20667 // Only append the chip and reset the chip buffer if the max chips limit isn't reached. 20668 if (this.hasMaxChipsReached()) return; 20669 20670 this.appendChip(item); 20671 this.resetChipBuffer(); 20672 } 20673 })); 20674 20675 this.$element.find('input') 20676 .on('focus',angular.bind(this, this.onInputFocus) ) 20677 .on('blur', angular.bind(this, this.onInputBlur) ); 20678 } 20679 }; 20680 20681 MdChipsCtrl.prototype.hasFocus = function () { 20682 return this.inputHasFocus || this.selectedChip >= 0; 20683 }; 20684 20685 })(); 20686 (function(){ 20687 "use strict"; 20688 20689 angular 20690 .module('material.components.chips') 20691 .directive('mdChips', MdChips); 20692 20693 /** 20694 * @ngdoc directive 20695 * @name mdChips 20696 * @module material.components.chips 20697 * 20698 * @description 20699 * `<md-chips>` is an input component for building lists of strings or objects. The list items are 20700 * displayed as 'chips'. This component can make use of an `<input>` element or an 20701 * `<md-autocomplete>` element. 20702 * 20703 * ### Custom templates 20704 * A custom template may be provided to render the content of each chip. This is achieved by 20705 * specifying an `<md-chip-template>` element containing the custom content as a child of 20706 * `<md-chips>`. 20707 * 20708 * Note: Any attributes on 20709 * `<md-chip-template>` will be dropped as only the innerHTML is used for the chip template. The 20710 * variables `$chip` and `$index` are available in the scope of `<md-chip-template>`, representing 20711 * the chip object and its index in the list of chips, respectively. 20712 * To override the chip delete control, include an element (ideally a button) with the attribute 20713 * `md-chip-remove`. A click listener to remove the chip will be added automatically. The element 20714 * is also placed as a sibling to the chip content (on which there are also click listeners) to 20715 * avoid a nested ng-click situation. 20716 * 20717 * <h3> Pending Features </h3> 20718 * <ul style="padding-left:20px;"> 20719 * 20720 * <ul>Style 20721 * <li>Colours for hover, press states (ripple?).</li> 20722 * </ul> 20723 * 20724 * <ul>Validation 20725 * <li>allow a validation callback</li> 20726 * <li>highlighting style for invalid chips</li> 20727 * </ul> 20728 * 20729 * <ul>Item mutation 20730 * <li>Support ` 20731 * <md-chip-edit>` template, show/hide the edit element on tap/click? double tap/double 20732 * click? 20733 * </li> 20734 * </ul> 20735 * 20736 * <ul>Truncation and Disambiguation (?) 20737 * <li>Truncate chip text where possible, but do not truncate entries such that two are 20738 * indistinguishable.</li> 20739 * </ul> 20740 * 20741 * <ul>Drag and Drop 20742 * <li>Drag and drop chips between related `<md-chips>` elements. 20743 * </li> 20744 * </ul> 20745 * </ul> 20746 * 20747 * <span style="font-size:.8em;text-align:center"> 20748 * Warning: This component is a WORK IN PROGRESS. If you use it now, 20749 * it will probably break on you in the future. 20750 * </span> 20751 * 20752 * @param {string=|object=} ng-model A model to bind the list of items to 20753 * @param {string=} placeholder Placeholder text that will be forwarded to the input. 20754 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input, 20755 * displayed when there is at least one item in the list 20756 * @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding 20757 * the input and delete buttons 20758 * @param {number=} md-max-chips The maximum number of chips allowed to add through user input. 20759 * <br/><br/>The validation property `md-max-chips` can be used when the max chips 20760 * amount is reached. 20761 * @param {expression} md-transform-chip An expression of form `myFunction($chip)` that when called 20762 * expects one of the following return values: 20763 * - an object representing the `$chip` input string 20764 * - `undefined` to simply add the `$chip` input string, or 20765 * - `null` to prevent the chip from being appended 20766 * @param {expression=} md-on-add An expression which will be called when a chip has been 20767 * added. 20768 * @param {expression=} md-on-remove An expression which will be called when a chip has been 20769 * removed. 20770 * @param {expression=} md-on-select An expression which will be called when a chip is selected. 20771 * @param {boolean} md-require-match If true, and the chips template contains an autocomplete, 20772 * only allow selection of pre-defined chips (i.e. you cannot add new ones). 20773 * @param {string=} delete-hint A string read by screen readers instructing users that pressing 20774 * the delete key will remove the chip. 20775 * @param {string=} delete-button-label A label for the delete button. Also hidden and read by 20776 * screen readers. 20777 * @param {expression=} md-separator-keys An array of key codes used to separate chips. 20778 * 20779 * @usage 20780 * <hljs lang="html"> 20781 * <md-chips 20782 * ng-model="myItems" 20783 * placeholder="Add an item" 20784 * readonly="isReadOnly"> 20785 * </md-chips> 20786 * </hljs> 20787 * 20788 * <h3>Validation</h3> 20789 * When using [ngMessages](https://docs.angularjs.org/api/ngMessages), you can show errors based 20790 * on our custom validators. 20791 * <hljs lang="html"> 20792 * <form name="userForm"> 20793 * <md-chips 20794 * name="fruits" 20795 * ng-model="myItems" 20796 * placeholder="Add an item" 20797 * md-max-chips="5"> 20798 * </md-chips> 20799 * <div ng-messages="userForm.fruits.$error" ng-if="userForm.$dirty"> 20800 * <div ng-message="md-max-chips">You reached the maximum amount of chips</div> 20801 * </div> 20802 * </form> 20803 * </hljs> 20804 * 20805 */ 20806 20807 20808 var MD_CHIPS_TEMPLATE = '\ 20809 <md-chips-wrap\ 20810 ng-if="!$mdChipsCtrl.readonly || $mdChipsCtrl.items.length > 0"\ 20811 ng-keydown="$mdChipsCtrl.chipKeydown($event)"\ 20812 ng-class="{ \'md-focused\': $mdChipsCtrl.hasFocus(), \'md-readonly\': !$mdChipsCtrl.ngModelCtrl }"\ 20813 class="md-chips">\ 20814 <md-chip ng-repeat="$chip in $mdChipsCtrl.items"\ 20815 index="{{$index}}"\ 20816 ng-class="{\'md-focused\': $mdChipsCtrl.selectedChip == $index, \'md-readonly\': $mdChipsCtrl.readonly}">\ 20817 <div class="md-chip-content"\ 20818 tabindex="-1"\ 20819 aria-hidden="true"\ 20820 ng-click="!$mdChipsCtrl.readonly && $mdChipsCtrl.focusChip($index)"\ 20821 ng-focus="!$mdChipsCtrl.readonly && $mdChipsCtrl.selectChip($index)"\ 20822 md-chip-transclude="$mdChipsCtrl.chipContentsTemplate"></div>\ 20823 <div ng-if="!$mdChipsCtrl.readonly"\ 20824 class="md-chip-remove-container"\ 20825 md-chip-transclude="$mdChipsCtrl.chipRemoveTemplate"></div>\ 20826 </md-chip>\ 20827 <div ng-if="!$mdChipsCtrl.readonly && $mdChipsCtrl.ngModelCtrl"\ 20828 class="md-chip-input-container"\ 20829 md-chip-transclude="$mdChipsCtrl.chipInputTemplate"></div>\ 20830 </div>\ 20831 </md-chips-wrap>'; 20832 20833 var CHIP_INPUT_TEMPLATE = '\ 20834 <input\ 20835 class="md-input"\ 20836 tabindex="0"\ 20837 placeholder="{{$mdChipsCtrl.getPlaceholder()}}"\ 20838 aria-label="{{$mdChipsCtrl.getPlaceholder()}}"\ 20839 ng-model="$mdChipsCtrl.chipBuffer"\ 20840 ng-focus="$mdChipsCtrl.onInputFocus()"\ 20841 ng-blur="$mdChipsCtrl.onInputBlur()"\ 20842 ng-trim="false"\ 20843 ng-keydown="$mdChipsCtrl.inputKeydown($event)">'; 20844 20845 var CHIP_DEFAULT_TEMPLATE = '\ 20846 <span>{{$chip}}</span>'; 20847 20848 var CHIP_REMOVE_TEMPLATE = '\ 20849 <button\ 20850 class="md-chip-remove"\ 20851 ng-if="!$mdChipsCtrl.readonly"\ 20852 ng-click="$mdChipsCtrl.removeChipAndFocusInput($$replacedScope.$index)"\ 20853 type="button"\ 20854 aria-hidden="true"\ 20855 tabindex="-1">\ 20856 <md-icon md-svg-icon="md-close"></md-icon>\ 20857 <span class="md-visually-hidden">\ 20858 {{$mdChipsCtrl.deleteButtonLabel}}\ 20859 </span>\ 20860 </button>'; 20861 20862 /** 20863 * MDChips Directive Definition 20864 */ 20865 function MdChips ($mdTheming, $mdUtil, $compile, $log, $timeout) { 20866 // Run our templates through $mdUtil.processTemplate() to allow custom start/end symbols 20867 var templates = getTemplates(); 20868 20869 return { 20870 template: function(element, attrs) { 20871 // Clone the element into an attribute. By prepending the attribute 20872 // name with '$', Angular won't write it into the DOM. The cloned 20873 // element propagates to the link function via the attrs argument, 20874 // where various contained-elements can be consumed. 20875 attrs['$mdUserTemplate'] = element.clone(); 20876 return templates.chips; 20877 }, 20878 require: ['mdChips'], 20879 restrict: 'E', 20880 controller: 'MdChipsCtrl', 20881 controllerAs: '$mdChipsCtrl', 20882 bindToController: true, 20883 compile: compile, 20884 scope: { 20885 readonly: '=readonly', 20886 placeholder: '@', 20887 secondaryPlaceholder: '@', 20888 maxChips: '@mdMaxChips', 20889 transformChip: '&mdTransformChip', 20890 onAppend: '&mdOnAppend', 20891 onAdd: '&mdOnAdd', 20892 onRemove: '&mdOnRemove', 20893 onSelect: '&mdOnSelect', 20894 deleteHint: '@', 20895 deleteButtonLabel: '@', 20896 separatorKeys: '=?mdSeparatorKeys', 20897 requireMatch: '=?mdRequireMatch' 20898 } 20899 }; 20900 20901 /** 20902 * Builds the final template for `md-chips` and returns the postLink function. 20903 * 20904 * Building the template involves 3 key components: 20905 * static chips 20906 * chip template 20907 * input control 20908 * 20909 * If no `ng-model` is provided, only the static chip work needs to be done. 20910 * 20911 * If no user-passed `md-chip-template` exists, the default template is used. This resulting 20912 * template is appended to the chip content element. 20913 * 20914 * The remove button may be overridden by passing an element with an md-chip-remove attribute. 20915 * 20916 * If an `input` or `md-autocomplete` element is provided by the caller, it is set aside for 20917 * transclusion later. The transclusion happens in `postLink` as the parent scope is required. 20918 * If no user input is provided, a default one is appended to the input container node in the 20919 * template. 20920 * 20921 * Static Chips (i.e. `md-chip` elements passed from the caller) are gathered and set aside for 20922 * transclusion in the `postLink` function. 20923 * 20924 * 20925 * @param element 20926 * @param attr 20927 * @returns {Function} 20928 */ 20929 function compile(element, attr) { 20930 // Grab the user template from attr and reset the attribute to null. 20931 var userTemplate = attr['$mdUserTemplate']; 20932 attr['$mdUserTemplate'] = null; 20933 20934 // Set the chip remove, chip contents and chip input templates. The link function will put 20935 // them on the scope for transclusion later. 20936 var chipRemoveTemplate = getTemplateByQuery('md-chips>*[md-chip-remove]') || templates.remove, 20937 chipContentsTemplate = getTemplateByQuery('md-chips>md-chip-template') || templates.default, 20938 chipInputTemplate = getTemplateByQuery('md-chips>md-autocomplete') 20939 || getTemplateByQuery('md-chips>input') 20940 || templates.input, 20941 staticChips = userTemplate.find('md-chip'); 20942 20943 // Warn of malformed template. See #2545 20944 if (userTemplate[0].querySelector('md-chip-template>*[md-chip-remove]')) { 20945 $log.warn('invalid placement of md-chip-remove within md-chip-template.'); 20946 } 20947 20948 function getTemplateByQuery (query) { 20949 if (!attr.ngModel) return; 20950 var element = userTemplate[0].querySelector(query); 20951 return element && element.outerHTML; 20952 } 20953 20954 /** 20955 * Configures controller and transcludes. 20956 */ 20957 return function postLink(scope, element, attrs, controllers) { 20958 $mdUtil.initOptionalProperties(scope, attr); 20959 20960 $mdTheming(element); 20961 var mdChipsCtrl = controllers[0]; 20962 mdChipsCtrl.chipContentsTemplate = chipContentsTemplate; 20963 mdChipsCtrl.chipRemoveTemplate = chipRemoveTemplate; 20964 mdChipsCtrl.chipInputTemplate = chipInputTemplate; 20965 20966 element 20967 .attr({ 'aria-hidden': true, tabindex: -1 }) 20968 .on('focus', function () { mdChipsCtrl.onFocus(); }); 20969 20970 if (attr.ngModel) { 20971 mdChipsCtrl.configureNgModel(element.controller('ngModel')); 20972 20973 // If an `md-transform-chip` attribute was set, tell the controller to use the expression 20974 // before appending chips. 20975 if (attrs.mdTransformChip) mdChipsCtrl.useTransformChipExpression(); 20976 20977 // If an `md-on-append` attribute was set, tell the controller to use the expression 20978 // when appending chips. 20979 // 20980 // DEPRECATED: Will remove in official 1.0 release 20981 if (attrs.mdOnAppend) mdChipsCtrl.useOnAppendExpression(); 20982 20983 // If an `md-on-add` attribute was set, tell the controller to use the expression 20984 // when adding chips. 20985 if (attrs.mdOnAdd) mdChipsCtrl.useOnAddExpression(); 20986 20987 // If an `md-on-remove` attribute was set, tell the controller to use the expression 20988 // when removing chips. 20989 if (attrs.mdOnRemove) mdChipsCtrl.useOnRemoveExpression(); 20990 20991 // If an `md-on-select` attribute was set, tell the controller to use the expression 20992 // when selecting chips. 20993 if (attrs.mdOnSelect) mdChipsCtrl.useOnSelectExpression(); 20994 20995 // The md-autocomplete and input elements won't be compiled until after this directive 20996 // is complete (due to their nested nature). Wait a tick before looking for them to 20997 // configure the controller. 20998 if (chipInputTemplate != templates.input) { 20999 // The autocomplete will not appear until the readonly attribute is not true (i.e. 21000 // false or undefined), so we have to watch the readonly and then on the next tick 21001 // after the chip transclusion has run, we can configure the autocomplete and user 21002 // input. 21003 scope.$watch('$mdChipsCtrl.readonly', function(readonly) { 21004 if (!readonly) { 21005 $mdUtil.nextTick(function(){ 21006 if (chipInputTemplate.indexOf('<md-autocomplete') === 0) 21007 mdChipsCtrl 21008 .configureAutocomplete(element.find('md-autocomplete') 21009 .controller('mdAutocomplete')); 21010 mdChipsCtrl.configureUserInput(element.find('input')); 21011 }); 21012 } 21013 }); 21014 } 21015 21016 // At the next tick, if we find an input, make sure it has the md-input class 21017 $mdUtil.nextTick(function() { 21018 var input = element.find('input'); 21019 21020 input && input.toggleClass('md-input', true); 21021 }); 21022 } 21023 21024 // Compile with the parent's scope and prepend any static chips to the wrapper. 21025 if (staticChips.length > 0) { 21026 var compiledStaticChips = $compile(staticChips.clone())(scope.$parent); 21027 $timeout(function() { element.find('md-chips-wrap').prepend(compiledStaticChips); }); 21028 } 21029 }; 21030 } 21031 21032 function getTemplates() { 21033 return { 21034 chips: $mdUtil.processTemplate(MD_CHIPS_TEMPLATE), 21035 input: $mdUtil.processTemplate(CHIP_INPUT_TEMPLATE), 21036 default: $mdUtil.processTemplate(CHIP_DEFAULT_TEMPLATE), 21037 remove: $mdUtil.processTemplate(CHIP_REMOVE_TEMPLATE) 21038 }; 21039 } 21040 } 21041 MdChips.$inject = ["$mdTheming", "$mdUtil", "$compile", "$log", "$timeout"]; 21042 21043 })(); 21044 (function(){ 21045 "use strict"; 21046 21047 angular 21048 .module('material.components.chips') 21049 .controller('MdContactChipsCtrl', MdContactChipsCtrl); 21050 21051 21052 21053 /** 21054 * Controller for the MdContactChips component 21055 * @constructor 21056 */ 21057 function MdContactChipsCtrl () { 21058 /** @type {Object} */ 21059 this.selectedItem = null; 21060 21061 /** @type {string} */ 21062 this.searchText = ''; 21063 } 21064 21065 21066 MdContactChipsCtrl.prototype.queryContact = function(searchText) { 21067 var results = this.contactQuery({'$query': searchText}); 21068 return this.filterSelected ? 21069 results.filter(angular.bind(this, this.filterSelectedContacts)) : results; 21070 }; 21071 21072 21073 MdContactChipsCtrl.prototype.itemName = function(item) { 21074 return item[this.contactName]; 21075 }; 21076 21077 21078 MdContactChipsCtrl.prototype.filterSelectedContacts = function(contact) { 21079 return this.contacts.indexOf(contact) == -1; 21080 }; 21081 21082 })(); 21083 (function(){ 21084 "use strict"; 21085 21086 angular 21087 .module('material.components.chips') 21088 .directive('mdContactChips', MdContactChips); 21089 21090 /** 21091 * @ngdoc directive 21092 * @name mdContactChips 21093 * @module material.components.chips 21094 * 21095 * @description 21096 * `<md-contact-chips>` is an input component based on `md-chips` and makes use of an 21097 * `md-autocomplete` element. The component allows the caller to supply a query expression which 21098 * returns a list of possible contacts. The user can select one of these and add it to the list of 21099 * chips. 21100 * 21101 * You may also use the `md-highlight-text` directive along with its parameters to control the 21102 * appearance of the matched text inside of the contacts' autocomplete popup. 21103 * 21104 * @param {string=|object=} ng-model A model to bind the list of items to 21105 * @param {string=} placeholder Placeholder text that will be forwarded to the input. 21106 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input, 21107 * displayed when there is at least on item in the list 21108 * @param {expression} md-contacts An expression expected to return contacts matching the search 21109 * test, `$query`. If this expression involves a promise, a loading bar is displayed while 21110 * waiting for it to resolve. 21111 * @param {string} md-contact-name The field name of the contact object representing the 21112 * contact's name. 21113 * @param {string} md-contact-email The field name of the contact object representing the 21114 * contact's email address. 21115 * @param {string} md-contact-image The field name of the contact object representing the 21116 * contact's image. 21117 * 21118 * 21119 * @param {expression=} filter-selected Whether to filter selected contacts from the list of 21120 * suggestions shown in the autocomplete. This attribute has been removed but may come back. 21121 * 21122 * 21123 * 21124 * @usage 21125 * <hljs lang="html"> 21126 * <md-contact-chips 21127 * ng-model="ctrl.contacts" 21128 * md-contacts="ctrl.querySearch($query)" 21129 * md-contact-name="name" 21130 * md-contact-image="image" 21131 * md-contact-email="email" 21132 * placeholder="To"> 21133 * </md-contact-chips> 21134 * </hljs> 21135 * 21136 */ 21137 21138 21139 var MD_CONTACT_CHIPS_TEMPLATE = '\ 21140 <md-chips class="md-contact-chips"\ 21141 ng-model="$mdContactChipsCtrl.contacts"\ 21142 md-require-match="$mdContactChipsCtrl.requireMatch"\ 21143 md-autocomplete-snap>\ 21144 <md-autocomplete\ 21145 md-menu-class="md-contact-chips-suggestions"\ 21146 md-selected-item="$mdContactChipsCtrl.selectedItem"\ 21147 md-search-text="$mdContactChipsCtrl.searchText"\ 21148 md-items="item in $mdContactChipsCtrl.queryContact($mdContactChipsCtrl.searchText)"\ 21149 md-item-text="$mdContactChipsCtrl.itemName(item)"\ 21150 md-no-cache="true"\ 21151 md-autoselect\ 21152 placeholder="{{$mdContactChipsCtrl.contacts.length == 0 ?\ 21153 $mdContactChipsCtrl.placeholder : $mdContactChipsCtrl.secondaryPlaceholder}}">\ 21154 <div class="md-contact-suggestion">\ 21155 <img \ 21156 ng-src="{{item[$mdContactChipsCtrl.contactImage]}}"\ 21157 alt="{{item[$mdContactChipsCtrl.contactName]}}"\ 21158 ng-if="item[$mdContactChipsCtrl.contactImage]" />\ 21159 <span class="md-contact-name" md-highlight-text="$mdContactChipsCtrl.searchText"\ 21160 md-highlight-flags="{{$mdContactChipsCtrl.highlightFlags}}">\ 21161 {{item[$mdContactChipsCtrl.contactName]}}\ 21162 </span>\ 21163 <span class="md-contact-email" >{{item[$mdContactChipsCtrl.contactEmail]}}</span>\ 21164 </div>\ 21165 </md-autocomplete>\ 21166 <md-chip-template>\ 21167 <div class="md-contact-avatar">\ 21168 <img \ 21169 ng-src="{{$chip[$mdContactChipsCtrl.contactImage]}}"\ 21170 alt="{{$chip[$mdContactChipsCtrl.contactName]}}"\ 21171 ng-if="$chip[$mdContactChipsCtrl.contactImage]" />\ 21172 </div>\ 21173 <div class="md-contact-name">\ 21174 {{$chip[$mdContactChipsCtrl.contactName]}}\ 21175 </div>\ 21176 </md-chip-template>\ 21177 </md-chips>'; 21178 21179 21180 /** 21181 * MDContactChips Directive Definition 21182 * 21183 * @param $mdTheming 21184 * @returns {*} 21185 * @ngInject 21186 */ 21187 function MdContactChips($mdTheming, $mdUtil) { 21188 return { 21189 template: function(element, attrs) { 21190 return MD_CONTACT_CHIPS_TEMPLATE; 21191 }, 21192 restrict: 'E', 21193 controller: 'MdContactChipsCtrl', 21194 controllerAs: '$mdContactChipsCtrl', 21195 bindToController: true, 21196 compile: compile, 21197 scope: { 21198 contactQuery: '&mdContacts', 21199 placeholder: '@', 21200 secondaryPlaceholder: '@', 21201 contactName: '@mdContactName', 21202 contactImage: '@mdContactImage', 21203 contactEmail: '@mdContactEmail', 21204 contacts: '=ngModel', 21205 requireMatch: '=?mdRequireMatch', 21206 highlightFlags: '@?mdHighlightFlags' 21207 } 21208 }; 21209 21210 function compile(element, attr) { 21211 return function postLink(scope, element, attrs, controllers) { 21212 21213 $mdUtil.initOptionalProperties(scope, attr); 21214 $mdTheming(element); 21215 21216 element.attr('tabindex', '-1'); 21217 }; 21218 } 21219 } 21220 MdContactChips.$inject = ["$mdTheming", "$mdUtil"]; 21221 21222 })(); 21223 (function(){ 21224 "use strict"; 21225 21226 angular 21227 .module('material.components.icon') 21228 .directive('mdIcon', ['$mdIcon', '$mdTheming', '$mdAria', mdIconDirective]); 21229 21230 /** 21231 * @ngdoc directive 21232 * @name mdIcon 21233 * @module material.components.icon 21234 * 21235 * @restrict E 21236 * 21237 * @description 21238 * The `md-icon` directive makes it easier to use vector-based icons in your app (as opposed to 21239 * raster-based icons types like PNG). The directive supports both icon fonts and SVG icons. 21240 * 21241 * Icons should be consider view-only elements that should not be used directly as buttons; instead nest a `<md-icon>` 21242 * inside a `md-button` to add hover and click features. 21243 * 21244 * ### Icon fonts 21245 * Icon fonts are a technique in which you use a font where the glyphs in the font are 21246 * your icons instead of text. Benefits include a straightforward way to bundle everything into a 21247 * single HTTP request, simple scaling, easy color changing, and more. 21248 * 21249 * `md-icon` lets you consume an icon font by letting you reference specific icons in that font 21250 * by name rather than character code. 21251 * 21252 * ### SVG 21253 * For SVGs, the problem with using `<img>` or a CSS `background-image` is that you can't take 21254 * advantage of some SVG features, such as styling specific parts of the icon with CSS or SVG 21255 * animation. 21256 * 21257 * `md-icon` makes it easier to use SVG icons by *inlining* the SVG into an `<svg>` element in the 21258 * document. The most straightforward way of referencing an SVG icon is via URL, just like a 21259 * traditional `<img>`. `$mdIconProvider`, as a convenience, lets you _name_ an icon so you can 21260 * reference it by name instead of URL throughout your templates. 21261 * 21262 * Additionally, you may not want to make separate HTTP requests for every icon, so you can bundle 21263 * your SVG icons together and pre-load them with $mdIconProvider as an icon set. An icon set can 21264 * also be given a name, which acts as a namespace for individual icons, so you can reference them 21265 * like `"social:cake"`. 21266 * 21267 * When using SVGs, both external SVGs (via URLs) or sets of SVGs [from icon sets] can be 21268 * easily loaded and used.When use font-icons, developers must following three (3) simple steps: 21269 * 21270 * <ol> 21271 * <li>Load the font library. e.g.<br/> 21272 * <link href="https://fonts.googleapis.com/icon?family=Material+Icons" 21273 * rel="stylesheet"> 21274 * </li> 21275 * <li> Use either (a) font-icon class names or (b) font ligatures to render the font glyph by using its textual name</li> 21276 * <li> Use <md-icon md-font-icon="classname" /> or <br/> 21277 * use <md-icon md-font-set="font library classname or alias"> textual_name </md-icon> or <br/> 21278 * use <md-icon md-font-set="font library classname or alias"> numerical_character_reference </md-icon> 21279 * </li> 21280 * </ol> 21281 * 21282 * Full details for these steps can be found: 21283 * 21284 * <ul> 21285 * <li>http://google.github.io/material-design-icons/</li> 21286 * <li>http://google.github.io/material-design-icons/#icon-font-for-the-web</li> 21287 * </ul> 21288 * 21289 * The Material Design icon style <code>.material-icons</code> and the icon font references are published in 21290 * Material Design Icons: 21291 * 21292 * <ul> 21293 * <li>http://www.google.com/design/icons/</li> 21294 * <li>https://www.google.com/design/icons/#ic_accessibility</li> 21295 * </ul> 21296 * 21297 * <h2 id="material_design_icons">Material Design Icons</h2> 21298 * Using the Material Design Icon-Selector, developers can easily and quickly search for a Material Design font-icon and 21299 * determine its textual name and character reference code. Click on any icon to see the slide-up information 21300 * panel with details regarding a SVG download or information on the font-icon usage. 21301 * 21302 * <a href="https://www.google.com/design/icons/#ic_accessibility" target="_blank" style="border-bottom:none;"> 21303 * <img src="https://cloud.githubusercontent.com/assets/210413/7902490/fe8dd14c-0780-11e5-98fb-c821cc6475e6.png" 21304 * aria-label="Material Design Icon-Selector" style="max-width:75%;padding-left:10%"> 21305 * </a> 21306 * 21307 * <span class="image_caption"> 21308 * Click on the image above to link to the 21309 * <a href="https://www.google.com/design/icons/#ic_accessibility" target="_blank">Material Design Icon-Selector</a>. 21310 * </span> 21311 * 21312 * @param {string} md-font-icon String name of CSS icon associated with the font-face will be used 21313 * to render the icon. Requires the fonts and the named CSS styles to be preloaded. 21314 * @param {string} md-font-set CSS style name associated with the font library; which will be assigned as 21315 * the class for the font-icon ligature. This value may also be an alias that is used to lookup the classname; 21316 * internally use `$mdIconProvider.fontSet(<alias>)` to determine the style name. 21317 * @param {string} md-svg-src String URL (or expression) used to load, cache, and display an 21318 * external SVG. 21319 * @param {string} md-svg-icon md-svg-icon String name used for lookup of the icon from the internal cache; 21320 * interpolated strings or expressions may also be used. Specific set names can be used with 21321 * the syntax `<set name>:<icon name>`.<br/><br/> 21322 * To use icon sets, developers are required to pre-register the sets using the `$mdIconProvider` service. 21323 * @param {string=} aria-label Labels icon for accessibility. If an empty string is provided, icon 21324 * will be hidden from accessibility layer with `aria-hidden="true"`. If there's no aria-label on the icon 21325 * nor a label on the parent element, a warning will be logged to the console. 21326 * @param {string=} alt Labels icon for accessibility. If an empty string is provided, icon 21327 * will be hidden from accessibility layer with `aria-hidden="true"`. If there's no alt on the icon 21328 * nor a label on the parent element, a warning will be logged to the console. 21329 * 21330 * @usage 21331 * When using SVGs: 21332 * <hljs lang="html"> 21333 * 21334 * <!-- Icon ID; may contain optional icon set prefix; icons must registered using $mdIconProvider --> 21335 * <md-icon md-svg-icon="social:android" aria-label="android " ></md-icon> 21336 * 21337 * <!-- Icon urls; may be preloaded in templateCache --> 21338 * <md-icon md-svg-src="/android.svg" aria-label="android " ></md-icon> 21339 * <md-icon md-svg-src="{{ getAndroid() }}" aria-label="android " ></md-icon> 21340 * 21341 * </hljs> 21342 * 21343 * Use the <code>$mdIconProvider</code> to configure your application with 21344 * svg iconsets. 21345 * 21346 * <hljs lang="js"> 21347 * angular.module('appSvgIconSets', ['ngMaterial']) 21348 * .controller('DemoCtrl', function($scope) {}) 21349 * .config(function($mdIconProvider) { 21350 * $mdIconProvider 21351 * .iconSet('social', 'img/icons/sets/social-icons.svg', 24) 21352 * .defaultIconSet('img/icons/sets/core-icons.svg', 24); 21353 * }); 21354 * </hljs> 21355 * 21356 * 21357 * When using Font Icons with classnames: 21358 * <hljs lang="html"> 21359 * 21360 * <md-icon md-font-icon="android" aria-label="android" ></md-icon> 21361 * <md-icon class="icon_home" aria-label="Home" ></md-icon> 21362 * 21363 * </hljs> 21364 * 21365 * When using Material Font Icons with ligatures: 21366 * <hljs lang="html"> 21367 * <!-- 21368 * For Material Design Icons 21369 * The class '.material-icons' is auto-added if a style has NOT been specified 21370 * since `material-icons` is the default fontset. So your markup: 21371 * --> 21372 * <md-icon> face </md-icon> 21373 * <!-- becomes this at runtime: --> 21374 * <md-icon md-font-set="material-icons"> face </md-icon> 21375 * <!-- If the fontset does not support ligature names, then we need to use the ligature unicode.--> 21376 * <md-icon>  </md-icon> 21377 * <!-- The class '.material-icons' must be manually added if other styles are also specified--> 21378 * <md-icon class="material-icons md-light md-48"> face </md-icon> 21379 * </hljs> 21380 * 21381 * When using other Font-Icon libraries: 21382 * 21383 * <hljs lang="js"> 21384 * // Specify a font-icon style alias 21385 * angular.config(function($mdIconProvider) { 21386 * $mdIconProvider.fontSet('md', 'material-icons'); 21387 * }); 21388 * </hljs> 21389 * 21390 * <hljs lang="html"> 21391 * <md-icon md-font-set="md">favorite</md-icon> 21392 * </hljs> 21393 * 21394 */ 21395 function mdIconDirective($mdIcon, $mdTheming, $mdAria ) { 21396 21397 return { 21398 restrict: 'E', 21399 link : postLink 21400 }; 21401 21402 21403 /** 21404 * Directive postLink 21405 * Supports embedded SVGs, font-icons, & external SVGs 21406 */ 21407 function postLink(scope, element, attr) { 21408 $mdTheming(element); 21409 21410 prepareForFontIcon(); 21411 21412 // If using a font-icon, then the textual name of the icon itself 21413 // provides the aria-label. 21414 21415 var label = attr.alt || attr.mdFontIcon || attr.mdSvgIcon || element.text(); 21416 var attrName = attr.$normalize(attr.$attr.mdSvgIcon || attr.$attr.mdSvgSrc || ''); 21417 21418 if ( !attr['aria-label'] ) { 21419 21420 if (label !== '' && !parentsHaveText() ) { 21421 21422 $mdAria.expect(element, 'aria-label', label); 21423 $mdAria.expect(element, 'role', 'img'); 21424 21425 } else if ( !element.text() ) { 21426 // If not a font-icon with ligature, then 21427 // hide from the accessibility layer. 21428 21429 $mdAria.expect(element, 'aria-hidden', 'true'); 21430 } 21431 } 21432 21433 if (attrName) { 21434 // Use either pre-configured SVG or URL source, respectively. 21435 attr.$observe(attrName, function(attrVal) { 21436 21437 element.empty(); 21438 if (attrVal) { 21439 $mdIcon(attrVal) 21440 .then(function(svg) { 21441 element.empty(); 21442 element.append(svg); 21443 }); 21444 } 21445 21446 }); 21447 } 21448 21449 function parentsHaveText() { 21450 var parent = element.parent(); 21451 if (parent.attr('aria-label') || parent.text()) { 21452 return true; 21453 } 21454 else if(parent.parent().attr('aria-label') || parent.parent().text()) { 21455 return true; 21456 } 21457 return false; 21458 } 21459 21460 function prepareForFontIcon() { 21461 if (!attr.mdSvgIcon && !attr.mdSvgSrc) { 21462 if (attr.mdFontIcon) { 21463 element.addClass('md-font ' + attr.mdFontIcon); 21464 } 21465 element.addClass($mdIcon.fontSet(attr.mdFontSet)); 21466 } 21467 } 21468 } 21469 } 21470 21471 })(); 21472 (function(){ 21473 "use strict"; 21474 21475 angular 21476 .module('material.components.icon' ) 21477 .provider('$mdIcon', MdIconProvider); 21478 21479 /** 21480 * @ngdoc service 21481 * @name $mdIconProvider 21482 * @module material.components.icon 21483 * 21484 * @description 21485 * `$mdIconProvider` is used only to register icon IDs with URLs. These configuration features allow 21486 * icons and icon sets to be pre-registered and associated with source URLs **before** the `<md-icon />` 21487 * directives are compiled. 21488 * 21489 * If using font-icons, the developer is responsible for loading the fonts. 21490 * 21491 * If using SVGs, loading of the actual svg files are deferred to on-demand requests and are loaded 21492 * internally by the `$mdIcon` service using the `$http` service. When an SVG is requested by name/ID, 21493 * the `$mdIcon` service searches its registry for the associated source URL; 21494 * that URL is used to on-demand load and parse the SVG dynamically. 21495 * 21496 * **Notice:** Most font-icons libraries do not support ligatures (for example `fontawesome`).<br/> 21497 * In such cases you are not able to use the icon's ligature name - Like so: 21498 * 21499 * <hljs lang="html"> 21500 * <md-icon md-font-set="fa">fa-bell</md-icon> 21501 * </hljs> 21502 * 21503 * You should instead use the given unicode, instead of the ligature name. 21504 * 21505 * <p ng-hide="true"> ##// Notice we can't use a hljs element here, because the characters will be escaped.</p> 21506 * ```html 21507 * <md-icon md-font-set="fa"></md-icon> 21508 * ``` 21509 * 21510 * All unicode ligatures are prefixed with the `&#x` string. 21511 * 21512 * @usage 21513 * <hljs lang="js"> 21514 * app.config(function($mdIconProvider) { 21515 * 21516 * // Configure URLs for icons specified by [set:]id. 21517 * 21518 * $mdIconProvider 21519 * .defaultFontSet( 'fa' ) // This sets our default fontset className. 21520 * .defaultIconSet('my/app/icons.svg') // Register a default set of SVG icons 21521 * .iconSet('social', 'my/app/social.svg') // Register a named icon set of SVGs 21522 * .icon('android', 'my/app/android.svg') // Register a specific icon (by name) 21523 * .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set 21524 * }); 21525 * </hljs> 21526 * 21527 * SVG icons and icon sets can be easily pre-loaded and cached using either (a) a build process or (b) a runtime 21528 * **startup** process (shown below): 21529 * 21530 * <hljs lang="js"> 21531 * app.config(function($mdIconProvider) { 21532 * 21533 * // Register a default set of SVG icon definitions 21534 * $mdIconProvider.defaultIconSet('my/app/icons.svg') 21535 * 21536 * }) 21537 * .run(function($http, $templateCache){ 21538 * 21539 * // Pre-fetch icons sources by URL and cache in the $templateCache... 21540 * // subsequent $http calls will look there first. 21541 * 21542 * var urls = [ 'imy/app/icons.svg', 'img/icons/android.svg']; 21543 * 21544 * angular.forEach(urls, function(url) { 21545 * $http.get(url, {cache: $templateCache}); 21546 * }); 21547 * 21548 * }); 21549 * 21550 * </hljs> 21551 * 21552 * NOTE: the loaded SVG data is subsequently cached internally for future requests. 21553 * 21554 */ 21555 21556 /** 21557 * @ngdoc method 21558 * @name $mdIconProvider#icon 21559 * 21560 * @description 21561 * Register a source URL for a specific icon name; the name may include optional 'icon set' name prefix. 21562 * These icons will later be retrieved from the cache using `$mdIcon( <icon name> )` 21563 * 21564 * @param {string} id Icon name/id used to register the icon 21565 * @param {string} url specifies the external location for the data file. Used internally by `$http` to load the 21566 * data or as part of the lookup in `$templateCache` if pre-loading was configured. 21567 * @param {number=} viewBoxSize Sets the width and height the icon's viewBox. 21568 * It is ignored for icons with an existing viewBox. Default size is 24. 21569 * 21570 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API 21571 * 21572 * @usage 21573 * <hljs lang="js"> 21574 * app.config(function($mdIconProvider) { 21575 * 21576 * // Configure URLs for icons specified by [set:]id. 21577 * 21578 * $mdIconProvider 21579 * .icon('android', 'my/app/android.svg') // Register a specific icon (by name) 21580 * .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set 21581 * }); 21582 * </hljs> 21583 * 21584 */ 21585 /** 21586 * @ngdoc method 21587 * @name $mdIconProvider#iconSet 21588 * 21589 * @description 21590 * Register a source URL for a 'named' set of icons; group of SVG definitions where each definition 21591 * has an icon id. Individual icons can be subsequently retrieved from this cached set using 21592 * `$mdIcon(<icon set name>:<icon name>)` 21593 * 21594 * @param {string} id Icon name/id used to register the iconset 21595 * @param {string} url specifies the external location for the data file. Used internally by `$http` to load the 21596 * data or as part of the lookup in `$templateCache` if pre-loading was configured. 21597 * @param {number=} viewBoxSize Sets the width and height of the viewBox of all icons in the set. 21598 * It is ignored for icons with an existing viewBox. All icons in the icon set should be the same size. 21599 * Default value is 24. 21600 * 21601 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API 21602 * 21603 * 21604 * @usage 21605 * <hljs lang="js"> 21606 * app.config(function($mdIconProvider) { 21607 * 21608 * // Configure URLs for icons specified by [set:]id. 21609 * 21610 * $mdIconProvider 21611 * .iconSet('social', 'my/app/social.svg') // Register a named icon set 21612 * }); 21613 * </hljs> 21614 * 21615 */ 21616 /** 21617 * @ngdoc method 21618 * @name $mdIconProvider#defaultIconSet 21619 * 21620 * @description 21621 * Register a source URL for the default 'named' set of icons. Unless explicitly registered, 21622 * subsequent lookups of icons will failover to search this 'default' icon set. 21623 * Icon can be retrieved from this cached, default set using `$mdIcon(<name>)` 21624 * 21625 * @param {string} url specifies the external location for the data file. Used internally by `$http` to load the 21626 * data or as part of the lookup in `$templateCache` if pre-loading was configured. 21627 * @param {number=} viewBoxSize Sets the width and height of the viewBox of all icons in the set. 21628 * It is ignored for icons with an existing viewBox. All icons in the icon set should be the same size. 21629 * Default value is 24. 21630 * 21631 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API 21632 * 21633 * @usage 21634 * <hljs lang="js"> 21635 * app.config(function($mdIconProvider) { 21636 * 21637 * // Configure URLs for icons specified by [set:]id. 21638 * 21639 * $mdIconProvider 21640 * .defaultIconSet( 'my/app/social.svg' ) // Register a default icon set 21641 * }); 21642 * </hljs> 21643 * 21644 */ 21645 /** 21646 * @ngdoc method 21647 * @name $mdIconProvider#defaultFontSet 21648 * 21649 * @description 21650 * When using Font-Icons, Angular Material assumes the the Material Design icons will be used and automatically 21651 * configures the default font-set == 'material-icons'. Note that the font-set references the font-icon library 21652 * class style that should be applied to the `<md-icon>`. 21653 * 21654 * Configuring the default means that the attributes 21655 * `md-font-set="material-icons"` or `class="material-icons"` do not need to be explicitly declared on the 21656 * `<md-icon>` markup. For example: 21657 * 21658 * `<md-icon> face </md-icon>` 21659 * will render as 21660 * `<span class="material-icons"> face </span>`, and 21661 * 21662 * `<md-icon md-font-set="fa"> face </md-icon>` 21663 * will render as 21664 * `<span class="fa"> face </span>` 21665 * 21666 * @param {string} name of the font-library style that should be applied to the md-icon DOM element 21667 * 21668 * @usage 21669 * <hljs lang="js"> 21670 * app.config(function($mdIconProvider) { 21671 * $mdIconProvider.defaultFontSet( 'fa' ); 21672 * }); 21673 * </hljs> 21674 * 21675 */ 21676 21677 /** 21678 * @ngdoc method 21679 * @name $mdIconProvider#fontSet 21680 * 21681 * @description 21682 * When using a font set for `<md-icon>` you must specify the correct font classname in the `md-font-set` 21683 * attribute. If the fonset className is really long, your markup may become cluttered... an easy 21684 * solution is to define an `alias` for your fontset: 21685 * 21686 * @param {string} alias of the specified fontset. 21687 * @param {string} className of the fontset. 21688 * 21689 * @usage 21690 * <hljs lang="js"> 21691 * app.config(function($mdIconProvider) { 21692 * // In this case, we set an alias for the `material-icons` fontset. 21693 * $mdIconProvider.fontSet('md', 'material-icons'); 21694 * }); 21695 * </hljs> 21696 * 21697 */ 21698 21699 /** 21700 * @ngdoc method 21701 * @name $mdIconProvider#defaultViewBoxSize 21702 * 21703 * @description 21704 * While `<md-icon />` markup can also be style with sizing CSS, this method configures 21705 * the default width **and** height used for all icons; unless overridden by specific CSS. 21706 * The default sizing is (24px, 24px). 21707 * @param {number=} viewBoxSize Sets the width and height of the viewBox for an icon or an icon set. 21708 * All icons in a set should be the same size. The default value is 24. 21709 * 21710 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API 21711 * 21712 * @usage 21713 * <hljs lang="js"> 21714 * app.config(function($mdIconProvider) { 21715 * 21716 * // Configure URLs for icons specified by [set:]id. 21717 * 21718 * $mdIconProvider 21719 * .defaultViewBoxSize(36) // Register a default icon size (width == height) 21720 * }); 21721 * </hljs> 21722 * 21723 */ 21724 21725 var config = { 21726 defaultViewBoxSize: 24, 21727 defaultFontSet: 'material-icons', 21728 fontSets : [ ] 21729 }; 21730 21731 function MdIconProvider() { } 21732 21733 MdIconProvider.prototype = { 21734 icon : function (id, url, viewBoxSize) { 21735 if ( id.indexOf(':') == -1 ) id = '$default:' + id; 21736 21737 config[id] = new ConfigurationItem(url, viewBoxSize ); 21738 return this; 21739 }, 21740 21741 iconSet : function (id, url, viewBoxSize) { 21742 config[id] = new ConfigurationItem(url, viewBoxSize ); 21743 return this; 21744 }, 21745 21746 defaultIconSet : function (url, viewBoxSize) { 21747 var setName = '$default'; 21748 21749 if ( !config[setName] ) { 21750 config[setName] = new ConfigurationItem(url, viewBoxSize ); 21751 } 21752 21753 config[setName].viewBoxSize = viewBoxSize || config.defaultViewBoxSize; 21754 21755 return this; 21756 }, 21757 21758 defaultViewBoxSize : function (viewBoxSize) { 21759 config.defaultViewBoxSize = viewBoxSize; 21760 return this; 21761 }, 21762 21763 /** 21764 * Register an alias name associated with a font-icon library style ; 21765 */ 21766 fontSet : function fontSet(alias, className) { 21767 config.fontSets.push({ 21768 alias : alias, 21769 fontSet : className || alias 21770 }); 21771 return this; 21772 }, 21773 21774 /** 21775 * Specify a default style name associated with a font-icon library 21776 * fallback to Material Icons. 21777 * 21778 */ 21779 defaultFontSet : function defaultFontSet(className) { 21780 config.defaultFontSet = !className ? '' : className; 21781 return this; 21782 }, 21783 21784 defaultIconSize : function defaultIconSize(iconSize) { 21785 config.defaultIconSize = iconSize; 21786 return this; 21787 }, 21788 21789 preloadIcons: function ($templateCache) { 21790 var iconProvider = this; 21791 var svgRegistry = [ 21792 { 21793 id : 'md-tabs-arrow', 21794 url: 'md-tabs-arrow.svg', 21795 svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 24 24"><g><polygon points="15.4,7.4 14,6 8,12 14,18 15.4,16.6 10.8,12 "/></g></svg>' 21796 }, 21797 { 21798 id : 'md-close', 21799 url: 'md-close.svg', 21800 svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 24 24"><g><path d="M19 6.41l-1.41-1.41-5.59 5.59-5.59-5.59-1.41 1.41 5.59 5.59-5.59 5.59 1.41 1.41 5.59-5.59 5.59 5.59 1.41-1.41-5.59-5.59z"/></g></svg>' 21801 }, 21802 { 21803 id: 'md-cancel', 21804 url: 'md-cancel.svg', 21805 svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 24 24"><g><path d="M12 2c-5.53 0-10 4.47-10 10s4.47 10 10 10 10-4.47 10-10-4.47-10-10-10zm5 13.59l-1.41 1.41-3.59-3.59-3.59 3.59-1.41-1.41 3.59-3.59-3.59-3.59 1.41-1.41 3.59 3.59 3.59-3.59 1.41 1.41-3.59 3.59 3.59 3.59z"/></g></svg>' 21806 }, 21807 { 21808 id: 'md-menu', 21809 url: 'md-menu.svg', 21810 svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 24 24"><path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" /></svg>' 21811 }, 21812 { 21813 id: 'md-toggle-arrow', 21814 url: 'md-toggle-arrow-svg', 21815 svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 48 48"><path d="M24 16l-12 12 2.83 2.83 9.17-9.17 9.17 9.17 2.83-2.83z"/><path d="M0 0h48v48h-48z" fill="none"/></svg>' 21816 }, 21817 { 21818 id: 'md-calendar', 21819 url: 'md-calendar.svg', 21820 svg: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"/></svg>' 21821 } 21822 ]; 21823 21824 svgRegistry.forEach(function(asset){ 21825 iconProvider.icon(asset.id, asset.url); 21826 $templateCache.put(asset.url, asset.svg); 21827 }); 21828 21829 }, 21830 21831 $get : ['$http', '$q', '$log', '$templateCache', '$mdUtil', function($http, $q, $log, $templateCache, $mdUtil) { 21832 this.preloadIcons($templateCache); 21833 return MdIconService(config, $http, $q, $log, $templateCache, $mdUtil); 21834 }] 21835 }; 21836 21837 /** 21838 * Configuration item stored in the Icon registry; used for lookups 21839 * to load if not already cached in the `loaded` cache 21840 */ 21841 function ConfigurationItem(url, viewBoxSize) { 21842 this.url = url; 21843 this.viewBoxSize = viewBoxSize || config.defaultViewBoxSize; 21844 } 21845 21846 /** 21847 * @ngdoc service 21848 * @name $mdIcon 21849 * @module material.components.icon 21850 * 21851 * @description 21852 * The `$mdIcon` service is a function used to lookup SVG icons. 21853 * 21854 * @param {string} id Query value for a unique Id or URL. If the argument is a URL, then the service will retrieve the icon element 21855 * from its internal cache or load the icon and cache it first. If the value is not a URL-type string, then an ID lookup is 21856 * performed. The Id may be a unique icon ID or may include an iconSet ID prefix. 21857 * 21858 * For the **id** query to work properly, this means that all id-to-URL mappings must have been previously configured 21859 * using the `$mdIconProvider`. 21860 * 21861 * @returns {obj} Clone of the initial SVG DOM element; which was created from the SVG markup in the SVG data file. 21862 * 21863 * @usage 21864 * <hljs lang="js"> 21865 * function SomeDirective($mdIcon) { 21866 * 21867 * // See if the icon has already been loaded, if not 21868 * // then lookup the icon from the registry cache, load and cache 21869 * // it for future requests. 21870 * // NOTE: ID queries require configuration with $mdIconProvider 21871 * 21872 * $mdIcon('android').then(function(iconEl) { element.append(iconEl); }); 21873 * $mdIcon('work:chair').then(function(iconEl) { element.append(iconEl); }); 21874 * 21875 * // Load and cache the external SVG using a URL 21876 * 21877 * $mdIcon('img/icons/android.svg').then(function(iconEl) { 21878 * element.append(iconEl); 21879 * }); 21880 * }; 21881 * </hljs> 21882 * 21883 * NOTE: The `<md-icon /> ` directive internally uses the `$mdIcon` service to query, loaded, and instantiate 21884 * SVG DOM elements. 21885 */ 21886 21887 /* @ngInject */ 21888 function MdIconService(config, $http, $q, $log, $templateCache, $mdUtil) { 21889 var iconCache = {}; 21890 var urlRegex = /[-\w@:%\+.~#?&//=]{2,}\.[a-z]{2,4}\b(\/[-\w@:%\+.~#?&//=]*)?/i; 21891 var dataUrlRegex = /^data:image\/svg\+xml[\s*;\w\-\=]*?(base64)?,(.*)$/i; 21892 21893 Icon.prototype = { clone : cloneSVG, prepare: prepareAndStyle }; 21894 getIcon.fontSet = findRegisteredFontSet; 21895 21896 // Publish service... 21897 return getIcon; 21898 21899 /** 21900 * Actual $mdIcon service is essentially a lookup function 21901 */ 21902 function getIcon(id) { 21903 id = id || ''; 21904 21905 // If already loaded and cached, use a clone of the cached icon. 21906 // Otherwise either load by URL, or lookup in the registry and then load by URL, and cache. 21907 21908 if ( iconCache[id] ) return $q.when( transformClone(iconCache[id]) ); 21909 if ( urlRegex.test(id) || dataUrlRegex.test(id) ) return loadByURL(id).then( cacheIcon(id) ); 21910 if ( id.indexOf(':') == -1 ) id = '$default:' + id; 21911 21912 var load = config[id] ? loadByID : loadFromIconSet; 21913 return load(id) 21914 .then( cacheIcon(id) ); 21915 } 21916 21917 /** 21918 * Lookup registered fontSet style using its alias... 21919 * If not found, 21920 */ 21921 function findRegisteredFontSet(alias) { 21922 var useDefault = angular.isUndefined(alias) || !(alias && alias.length); 21923 if ( useDefault ) return config.defaultFontSet; 21924 21925 var result = alias; 21926 angular.forEach(config.fontSets, function(it){ 21927 if ( it.alias == alias ) result = it.fontSet || result; 21928 }); 21929 21930 return result; 21931 } 21932 21933 function transformClone(cacheElement) { 21934 var clone = cacheElement.clone(); 21935 var cacheSuffix = '_cache' + $mdUtil.nextUid(); 21936 21937 // We need to modify for each cached icon the id attributes. 21938 // This is needed because SVG id's are treated as normal DOM ids 21939 // and should not have a duplicated id. 21940 if (clone.id) clone.id += cacheSuffix; 21941 angular.forEach(clone.querySelectorAll('[id]'), function (item) { 21942 item.id += cacheSuffix; 21943 }); 21944 21945 return clone; 21946 } 21947 21948 /** 21949 * Prepare and cache the loaded icon for the specified `id` 21950 */ 21951 function cacheIcon( id ) { 21952 21953 return function updateCache( icon ) { 21954 iconCache[id] = isIcon(icon) ? icon : new Icon(icon, config[id]); 21955 21956 return iconCache[id].clone(); 21957 }; 21958 } 21959 21960 /** 21961 * Lookup the configuration in the registry, if !registered throw an error 21962 * otherwise load the icon [on-demand] using the registered URL. 21963 * 21964 */ 21965 function loadByID(id) { 21966 var iconConfig = config[id]; 21967 return loadByURL(iconConfig.url).then(function(icon) { 21968 return new Icon(icon, iconConfig); 21969 }); 21970 } 21971 21972 /** 21973 * Loads the file as XML and uses querySelector( <id> ) to find 21974 * the desired node... 21975 */ 21976 function loadFromIconSet(id) { 21977 var setName = id.substring(0, id.lastIndexOf(':')) || '$default'; 21978 var iconSetConfig = config[setName]; 21979 21980 return !iconSetConfig ? announceIdNotFound(id) : loadByURL(iconSetConfig.url).then(extractFromSet); 21981 21982 function extractFromSet(set) { 21983 var iconName = id.slice(id.lastIndexOf(':') + 1); 21984 var icon = set.querySelector('#' + iconName); 21985 return !icon ? announceIdNotFound(id) : new Icon(icon, iconSetConfig); 21986 } 21987 21988 function announceIdNotFound(id) { 21989 var msg = 'icon ' + id + ' not found'; 21990 $log.warn(msg); 21991 21992 return $q.reject(msg || id); 21993 } 21994 } 21995 21996 /** 21997 * Load the icon by URL (may use the $templateCache). 21998 * Extract the data for later conversion to Icon 21999 */ 22000 function loadByURL(url) { 22001 /* Load the icon from embedded data URL. */ 22002 function loadByDataUrl(url) { 22003 var results = dataUrlRegex.exec(url); 22004 var isBase64 = /base64/i.test(url); 22005 var data = isBase64 ? window.atob(results[2]) : results[2]; 22006 return $q.when(angular.element(data)[0]); 22007 } 22008 22009 /* Load the icon by URL using HTTP. */ 22010 function loadByHttpUrl(url) { 22011 return $http 22012 .get(url, { cache: $templateCache }) 22013 .then(function(response) { 22014 return angular.element('<div>').append(response.data).find('svg')[0]; 22015 }).catch(announceNotFound); 22016 } 22017 22018 return dataUrlRegex.test(url) 22019 ? loadByDataUrl(url) 22020 : loadByHttpUrl(url); 22021 } 22022 22023 /** 22024 * Catch HTTP or generic errors not related to incorrect icon IDs. 22025 */ 22026 function announceNotFound(err) { 22027 var msg = angular.isString(err) ? err : (err.message || err.data || err.statusText); 22028 $log.warn(msg); 22029 22030 return $q.reject(msg); 22031 } 22032 22033 /** 22034 * Check target signature to see if it is an Icon instance. 22035 */ 22036 function isIcon(target) { 22037 return angular.isDefined(target.element) && angular.isDefined(target.config); 22038 } 22039 22040 /** 22041 * Define the Icon class 22042 */ 22043 function Icon(el, config) { 22044 if (el && el.tagName != 'svg') { 22045 el = angular.element('<svg xmlns="http://www.w3.org/2000/svg">').append(el)[0]; 22046 } 22047 22048 // Inject the namespace if not available... 22049 if ( !el.getAttribute('xmlns') ) { 22050 el.setAttribute('xmlns', "http://www.w3.org/2000/svg"); 22051 } 22052 22053 this.element = el; 22054 this.config = config; 22055 this.prepare(); 22056 } 22057 22058 /** 22059 * Prepare the DOM element that will be cached in the 22060 * loaded iconCache store. 22061 */ 22062 function prepareAndStyle() { 22063 var viewBoxSize = this.config ? this.config.viewBoxSize : config.defaultViewBoxSize; 22064 angular.forEach({ 22065 'fit' : '', 22066 'height': '100%', 22067 'width' : '100%', 22068 'preserveAspectRatio': 'xMidYMid meet', 22069 'viewBox' : this.element.getAttribute('viewBox') || ('0 0 ' + viewBoxSize + ' ' + viewBoxSize), 22070 'focusable': false // Disable IE11s default behavior to make SVGs focusable 22071 }, function(val, attr) { 22072 this.element.setAttribute(attr, val); 22073 }, this); 22074 } 22075 22076 /** 22077 * Clone the Icon DOM element. 22078 */ 22079 function cloneSVG(){ 22080 // If the element or any of its children have a style attribute, then a CSP policy without 22081 // 'unsafe-inline' in the style-src directive, will result in a violation. 22082 return this.element.cloneNode(true); 22083 } 22084 22085 } 22086 MdIconService.$inject = ["config", "$http", "$q", "$log", "$templateCache", "$mdUtil"]; 22087 22088 })(); 22089 (function(){ 22090 "use strict"; 22091 22092 22093 22094 angular 22095 .module('material.components.menu') 22096 .controller('mdMenuCtrl', MenuController); 22097 22098 /** 22099 * @ngInject 22100 */ 22101 function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $rootScope, $q) { 22102 22103 var menuContainer; 22104 var self = this; 22105 var triggerElement; 22106 22107 this.nestLevel = parseInt($attrs.mdNestLevel, 10) || 0; 22108 22109 /** 22110 * Called by our linking fn to provide access to the menu-content 22111 * element removed during link 22112 */ 22113 this.init = function init(setMenuContainer, opts) { 22114 opts = opts || {}; 22115 menuContainer = setMenuContainer; 22116 // Default element for ARIA attributes has the ngClick or ngMouseenter expression 22117 triggerElement = $element[0].querySelector('[ng-click],[ng-mouseenter]'); 22118 triggerElement.setAttribute('aria-expanded', 'false'); 22119 22120 this.isInMenuBar = opts.isInMenuBar; 22121 this.nestedMenus = $mdUtil.nodesToArray(menuContainer[0].querySelectorAll('.md-nested-menu')); 22122 22123 menuContainer.on('$mdInterimElementRemove', function() { 22124 self.isOpen = false; 22125 }); 22126 22127 var menuContainerId = 'menu_container_' + $mdUtil.nextUid(); 22128 menuContainer.attr('id', menuContainerId); 22129 angular.element(triggerElement).attr({ 22130 'aria-owns': menuContainerId, 22131 'aria-haspopup': 'true' 22132 }); 22133 22134 $scope.$on('$destroy', this.disableHoverListener); 22135 menuContainer.on('$destroy', function() { 22136 $mdMenu.destroy(); 22137 }); 22138 }; 22139 22140 var openMenuTimeout, menuItems, deregisterScopeListeners = []; 22141 this.enableHoverListener = function() { 22142 deregisterScopeListeners.push($rootScope.$on('$mdMenuOpen', function(event, el) { 22143 if (menuContainer[0].contains(el[0])) { 22144 self.currentlyOpenMenu = el.controller('mdMenu'); 22145 self.isAlreadyOpening = false; 22146 self.currentlyOpenMenu.registerContainerProxy(self.triggerContainerProxy.bind(self)); 22147 } 22148 })); 22149 deregisterScopeListeners.push($rootScope.$on('$mdMenuClose', function(event, el) { 22150 if (menuContainer[0].contains(el[0])) { 22151 self.currentlyOpenMenu = undefined; 22152 } 22153 })); 22154 menuItems = angular.element($mdUtil.nodesToArray(menuContainer[0].children[0].children)); 22155 menuItems.on('mouseenter', self.handleMenuItemHover); 22156 menuItems.on('mouseleave', self.handleMenuItemMouseLeave); 22157 }; 22158 22159 this.disableHoverListener = function() { 22160 while (deregisterScopeListeners.length) { 22161 deregisterScopeListeners.shift()(); 22162 } 22163 menuItems && menuItems.off('mouseenter', self.handleMenuItemHover); 22164 menuItems && menuItems.off('mouseleave', self.handleMenuMouseLeave); 22165 }; 22166 22167 this.handleMenuItemHover = function(event) { 22168 if (self.isAlreadyOpening) return; 22169 var nestedMenu = ( 22170 event.target.querySelector('md-menu') 22171 || $mdUtil.getClosest(event.target, 'MD-MENU') 22172 ); 22173 openMenuTimeout = $timeout(function() { 22174 if (nestedMenu) { 22175 nestedMenu = angular.element(nestedMenu).controller('mdMenu'); 22176 } 22177 22178 if (self.currentlyOpenMenu && self.currentlyOpenMenu != nestedMenu) { 22179 var closeTo = self.nestLevel + 1; 22180 self.currentlyOpenMenu.close(true, { closeTo: closeTo }); 22181 self.isAlreadyOpening = true; 22182 nestedMenu.open(); 22183 } else if (nestedMenu && !nestedMenu.isOpen && nestedMenu.open) { 22184 self.isAlreadyOpening = true; 22185 nestedMenu.open(); 22186 } 22187 }, nestedMenu ? 100 : 250); 22188 var focusableTarget = event.currentTarget.querySelector('.md-button:not([disabled])'); 22189 focusableTarget && focusableTarget.focus(); 22190 }; 22191 22192 this.handleMenuItemMouseLeave = function() { 22193 if (openMenuTimeout) { 22194 $timeout.cancel(openMenuTimeout); 22195 openMenuTimeout = undefined; 22196 } 22197 }; 22198 22199 22200 /** 22201 * Uses the $mdMenu interim element service to open the menu contents 22202 */ 22203 this.open = function openMenu(ev) { 22204 ev && ev.stopPropagation(); 22205 ev && ev.preventDefault(); 22206 if (self.isOpen) return; 22207 self.enableHoverListener(); 22208 self.isOpen = true; 22209 triggerElement = triggerElement || (ev ? ev.target : $element[0]); 22210 triggerElement.setAttribute('aria-expanded', 'true'); 22211 $scope.$emit('$mdMenuOpen', $element); 22212 $mdMenu.show({ 22213 scope: $scope, 22214 mdMenuCtrl: self, 22215 nestLevel: self.nestLevel, 22216 element: menuContainer, 22217 target: triggerElement, 22218 preserveElement: true, 22219 parent: 'body' 22220 }).finally(function() { 22221 triggerElement.setAttribute('aria-expanded', 'false'); 22222 self.disableHoverListener(); 22223 }); 22224 }; 22225 22226 // Expose a open function to the child scope for html to use 22227 $scope.$mdOpenMenu = this.open; 22228 22229 $scope.$watch(function() { return self.isOpen; }, function(isOpen) { 22230 if (isOpen) { 22231 menuContainer.attr('aria-hidden', 'false'); 22232 $element[0].classList.add('md-open'); 22233 angular.forEach(self.nestedMenus, function(el) { 22234 el.classList.remove('md-open'); 22235 }); 22236 } else { 22237 menuContainer.attr('aria-hidden', 'true'); 22238 $element[0].classList.remove('md-open'); 22239 } 22240 $scope.$mdMenuIsOpen = self.isOpen; 22241 }); 22242 22243 this.focusMenuContainer = function focusMenuContainer() { 22244 var focusTarget = menuContainer[0].querySelector('[md-menu-focus-target]'); 22245 if (!focusTarget) focusTarget = menuContainer[0].querySelector('.md-button'); 22246 focusTarget.focus(); 22247 }; 22248 22249 this.registerContainerProxy = function registerContainerProxy(handler) { 22250 this.containerProxy = handler; 22251 }; 22252 22253 this.triggerContainerProxy = function triggerContainerProxy(ev) { 22254 this.containerProxy && this.containerProxy(ev); 22255 }; 22256 22257 this.destroy = function() { 22258 return self.isOpen ? $mdMenu.destroy() : $q.when(false); 22259 }; 22260 22261 // Use the $mdMenu interim element service to close the menu contents 22262 this.close = function closeMenu(skipFocus, closeOpts) { 22263 if ( !self.isOpen ) return; 22264 self.isOpen = false; 22265 22266 var eventDetails = angular.extend({}, closeOpts, { skipFocus: skipFocus }); 22267 $scope.$emit('$mdMenuClose', $element, eventDetails); 22268 $mdMenu.hide(null, closeOpts); 22269 22270 if (!skipFocus) { 22271 var el = self.restoreFocusTo || $element.find('button')[0]; 22272 if (el instanceof angular.element) el = el[0]; 22273 if (el) el.focus(); 22274 } 22275 }; 22276 22277 /** 22278 * Build a nice object out of our string attribute which specifies the 22279 * target mode for left and top positioning 22280 */ 22281 this.positionMode = function positionMode() { 22282 var attachment = ($attrs.mdPositionMode || 'target').split(' '); 22283 22284 // If attachment is a single item, duplicate it for our second value. 22285 // ie. 'target' -> 'target target' 22286 if (attachment.length == 1) { 22287 attachment.push(attachment[0]); 22288 } 22289 22290 return { 22291 left: attachment[0], 22292 top: attachment[1] 22293 }; 22294 }; 22295 22296 /** 22297 * Build a nice object out of our string attribute which specifies 22298 * the offset of top and left in pixels. 22299 */ 22300 this.offsets = function offsets() { 22301 var position = ($attrs.mdOffset || '0 0').split(' ').map(parseFloat); 22302 if (position.length == 2) { 22303 return { 22304 left: position[0], 22305 top: position[1] 22306 }; 22307 } else if (position.length == 1) { 22308 return { 22309 top: position[0], 22310 left: position[0] 22311 }; 22312 } else { 22313 throw Error('Invalid offsets specified. Please follow format <x, y> or <n>'); 22314 } 22315 }; 22316 } 22317 MenuController.$inject = ["$mdMenu", "$attrs", "$element", "$scope", "$mdUtil", "$timeout", "$rootScope", "$q"]; 22318 22319 })(); 22320 (function(){ 22321 "use strict"; 22322 22323 /** 22324 * @ngdoc directive 22325 * @name mdMenu 22326 * @module material.components.menu 22327 * @restrict E 22328 * @description 22329 * 22330 * Menus are elements that open when clicked. They are useful for displaying 22331 * additional options within the context of an action. 22332 * 22333 * Every `md-menu` must specify exactly two child elements. The first element is what is 22334 * left in the DOM and is used to open the menu. This element is called the trigger element. 22335 * The trigger element's scope has access to `$mdOpenMenu($event)` 22336 * which it may call to open the menu. By passing $event as argument, the 22337 * corresponding event is stopped from propagating up the DOM-tree. 22338 * 22339 * The second element is the `md-menu-content` element which represents the 22340 * contents of the menu when it is open. Typically this will contain `md-menu-item`s, 22341 * but you can do custom content as well. 22342 * 22343 * <hljs lang="html"> 22344 * <md-menu> 22345 * <!-- Trigger element is a md-button with an icon --> 22346 * <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button" aria-label="Open sample menu"> 22347 * <md-icon md-svg-icon="call:phone"></md-icon> 22348 * </md-button> 22349 * <md-menu-content> 22350 * <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item> 22351 * </md-menu-content> 22352 * </md-menu> 22353 * </hljs> 22354 22355 * ## Sizing Menus 22356 * 22357 * The width of the menu when it is open may be specified by specifying a `width` 22358 * attribute on the `md-menu-content` element. 22359 * See the [Material Design Spec](http://www.google.com/design/spec/components/menus.html#menus-specs) 22360 * for more information. 22361 * 22362 * 22363 * ## Aligning Menus 22364 * 22365 * When a menu opens, it is important that the content aligns with the trigger element. 22366 * Failure to align menus can result in jarring experiences for users as content 22367 * suddenly shifts. To help with this, `md-menu` provides several APIs to help 22368 * with alignment. 22369 * 22370 * ### Target Mode 22371 * 22372 * By default, `md-menu` will attempt to align the `md-menu-content` by aligning 22373 * designated child elements in both the trigger and the menu content. 22374 * 22375 * To specify the alignment element in the `trigger` you can use the `md-menu-origin` 22376 * attribute on a child element. If no `md-menu-origin` is specified, the `md-menu` 22377 * will be used as the origin element. 22378 * 22379 * Similarly, the `md-menu-content` may specify a `md-menu-align-target` for a 22380 * `md-menu-item` to specify the node that it should try and align with. 22381 * 22382 * In this example code, we specify an icon to be our origin element, and an 22383 * icon in our menu content to be our alignment target. This ensures that both 22384 * icons are aligned when the menu opens. 22385 * 22386 * <hljs lang="html"> 22387 * <md-menu> 22388 * <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button" aria-label="Open some menu"> 22389 * <md-icon md-menu-origin md-svg-icon="call:phone"></md-icon> 22390 * </md-button> 22391 * <md-menu-content> 22392 * <md-menu-item> 22393 * <md-button ng-click="doSomething()" aria-label="Do something"> 22394 * <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon> 22395 * Do Something 22396 * </md-button> 22397 * </md-menu-item> 22398 * </md-menu-content> 22399 * </md-menu> 22400 * </hljs> 22401 * 22402 * Sometimes we want to specify alignment on the right side of an element, for example 22403 * if we have a menu on the right side a toolbar, we want to right align our menu content. 22404 * 22405 * We can specify the origin by using the `md-position-mode` attribute on both 22406 * the `x` and `y` axis. Right now only the `x-axis` has more than one option. 22407 * You may specify the default mode of `target target` or 22408 * `target-right target` to specify a right-oriented alignment target. See the 22409 * position section of the demos for more examples. 22410 * 22411 * ### Menu Offsets 22412 * 22413 * It is sometimes unavoidable to need to have a deeper level of control for 22414 * the positioning of a menu to ensure perfect alignment. `md-menu` provides 22415 * the `md-offset` attribute to allow pixel level specificty of adjusting the 22416 * exact positioning. 22417 * 22418 * This offset is provided in the format of `x y` or `n` where `n` will be used 22419 * in both the `x` and `y` axis. 22420 * 22421 * For example, to move a menu by `2px` from the top, we can use: 22422 * <hljs lang="html"> 22423 * <md-menu md-offset="2 0"> 22424 * <!-- menu-content --> 22425 * </md-menu> 22426 * </hljs> 22427 22428 * ### Preventing close 22429 * 22430 * Sometimes you would like to be able to click on a menu item without having the menu 22431 * close. To do this, ngMaterial exposes the `md-prevent-menu-close` attribute which 22432 * can be added to a button inside a menu to stop the menu from automatically closing. 22433 * You can then close the menu programatically by injecting `$mdMenu` and calling 22434 * `$mdMenu.hide()`. 22435 * 22436 * <hljs lang="html"> 22437 * <md-menu-item> 22438 * <md-button ng-click="doSomething()" aria-label="Do something" md-prevent-menu-close="md-prevent-menu-close"> 22439 * <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon> 22440 * Do Something 22441 * </md-button> 22442 * </md-menu-item> 22443 * </hljs> 22444 * 22445 * @usage 22446 * <hljs lang="html"> 22447 * <md-menu> 22448 * <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button"> 22449 * <md-icon md-svg-icon="call:phone"></md-icon> 22450 * </md-button> 22451 * <md-menu-content> 22452 * <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item> 22453 * </md-menu-content> 22454 * </md-menu> 22455 * </hljs> 22456 * 22457 * @param {string} md-position-mode The position mode in the form of 22458 * `x`, `y`. Default value is `target`,`target`. Right now the `x` axis 22459 * also supports `target-right`. 22460 * @param {string} md-offset An offset to apply to the dropdown after positioning 22461 * `x`, `y`. Default value is `0`,`0`. 22462 * 22463 */ 22464 22465 angular 22466 .module('material.components.menu') 22467 .directive('mdMenu', MenuDirective); 22468 22469 /** 22470 * @ngInject 22471 */ 22472 function MenuDirective($mdUtil) { 22473 var INVALID_PREFIX = 'Invalid HTML for md-menu: '; 22474 return { 22475 restrict: 'E', 22476 require: ['mdMenu', '?^mdMenuBar'], 22477 controller: 'mdMenuCtrl', // empty function to be built by link 22478 scope: true, 22479 compile: compile 22480 }; 22481 22482 function compile(templateElement) { 22483 templateElement.addClass('md-menu'); 22484 var triggerElement = templateElement.children()[0]; 22485 if (!triggerElement.hasAttribute('ng-click')) { 22486 triggerElement = triggerElement.querySelector('[ng-click],[ng-mouseenter]') || triggerElement; 22487 } 22488 if (triggerElement && ( 22489 triggerElement.nodeName == 'MD-BUTTON' || 22490 triggerElement.nodeName == 'BUTTON' 22491 ) && !triggerElement.hasAttribute('type')) { 22492 triggerElement.setAttribute('type', 'button'); 22493 } 22494 22495 if (templateElement.children().length != 2) { 22496 throw Error(INVALID_PREFIX + 'Expected two children elements.'); 22497 } 22498 22499 // Default element for ARIA attributes has the ngClick or ngMouseenter expression 22500 triggerElement && triggerElement.setAttribute('aria-haspopup', 'true'); 22501 22502 var nestedMenus = templateElement[0].querySelectorAll('md-menu'); 22503 var nestingDepth = parseInt(templateElement[0].getAttribute('md-nest-level'), 10) || 0; 22504 if (nestedMenus) { 22505 angular.forEach($mdUtil.nodesToArray(nestedMenus), function(menuEl) { 22506 if (!menuEl.hasAttribute('md-position-mode')) { 22507 menuEl.setAttribute('md-position-mode', 'cascade'); 22508 } 22509 menuEl.classList.add('md-nested-menu'); 22510 menuEl.setAttribute('md-nest-level', nestingDepth + 1); 22511 }); 22512 } 22513 return link; 22514 } 22515 22516 function link(scope, element, attrs, ctrls) { 22517 var mdMenuCtrl = ctrls[0]; 22518 var isInMenuBar = ctrls[1] != undefined; 22519 // Move everything into a md-menu-container and pass it to the controller 22520 var menuContainer = angular.element( 22521 '<div class="md-open-menu-container md-whiteframe-z2"></div>' 22522 ); 22523 var menuContents = element.children()[1]; 22524 if (!menuContents.hasAttribute('role')) { 22525 menuContents.setAttribute('role', 'menu'); 22526 } 22527 menuContainer.append(menuContents); 22528 22529 element.on('$destroy', function() { 22530 menuContainer.remove(); 22531 }); 22532 22533 element.append(menuContainer); 22534 menuContainer[0].style.display = 'none'; 22535 mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar }); 22536 } 22537 } 22538 MenuDirective.$inject = ["$mdUtil"]; 22539 22540 })(); 22541 (function(){ 22542 "use strict"; 22543 22544 angular 22545 .module('material.components.menu') 22546 .provider('$mdMenu', MenuProvider); 22547 22548 /* 22549 * Interim element provider for the menu. 22550 * Handles behavior for a menu while it is open, including: 22551 * - handling animating the menu opening/closing 22552 * - handling key/mouse events on the menu element 22553 * - handling enabling/disabling scroll while the menu is open 22554 * - handling redrawing during resizes and orientation changes 22555 * 22556 */ 22557 22558 function MenuProvider($$interimElementProvider) { 22559 var MENU_EDGE_MARGIN = 8; 22560 22561 menuDefaultOptions.$inject = ["$mdUtil", "$mdTheming", "$mdConstant", "$document", "$window", "$q", "$$rAF", "$animateCss", "$animate"]; 22562 return $$interimElementProvider('$mdMenu') 22563 .setDefaults({ 22564 methods: ['target'], 22565 options: menuDefaultOptions 22566 }); 22567 22568 /* @ngInject */ 22569 function menuDefaultOptions($mdUtil, $mdTheming, $mdConstant, $document, $window, $q, $$rAF, $animateCss, $animate) { 22570 var animator = $mdUtil.dom.animator; 22571 22572 return { 22573 parent: 'body', 22574 onShow: onShow, 22575 onRemove: onRemove, 22576 hasBackdrop: true, 22577 disableParentScroll: true, 22578 skipCompile: true, 22579 preserveScope: true, 22580 skipHide: true, 22581 themable: true 22582 }; 22583 22584 /** 22585 * Show modal backdrop element... 22586 * @returns {function(): void} A function that removes this backdrop 22587 */ 22588 function showBackdrop(scope, element, options) { 22589 if (options.nestLevel) return angular.noop; 22590 22591 // If we are not within a dialog... 22592 if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) { 22593 // !! DO this before creating the backdrop; since disableScrollAround() 22594 // configures the scroll offset; which is used by mdBackDrop postLink() 22595 options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent); 22596 } else { 22597 options.disableParentScroll = false; 22598 } 22599 22600 if (options.hasBackdrop) { 22601 options.backdrop = $mdUtil.createBackdrop(scope, "md-menu-backdrop md-click-catcher"); 22602 22603 $animate.enter(options.backdrop, $document[0].body); 22604 } 22605 22606 /** 22607 * Hide and destroys the backdrop created by showBackdrop() 22608 */ 22609 return function hideBackdrop() { 22610 if (options.backdrop) options.backdrop.remove(); 22611 if (options.disableParentScroll) options.restoreScroll(); 22612 }; 22613 } 22614 22615 /** 22616 * Removing the menu element from the DOM and remove all associated event listeners 22617 * and backdrop 22618 */ 22619 function onRemove(scope, element, opts) { 22620 opts.cleanupInteraction(); 22621 opts.cleanupResizing(); 22622 opts.hideBackdrop(); 22623 22624 // For navigation $destroy events, do a quick, non-animated removal, 22625 // but for normal closes (from clicks, etc) animate the removal 22626 22627 return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then( detachAndClean ); 22628 22629 /** 22630 * For normal closes, animate the removal. 22631 * For forced closes (like $destroy events), skip the animations 22632 */ 22633 function animateRemoval() { 22634 return $animateCss(element, {addClass: 'md-leave'}).start(); 22635 } 22636 22637 /** 22638 * Detach the element 22639 */ 22640 function detachAndClean() { 22641 element.removeClass('md-active'); 22642 detachElement(element, opts); 22643 opts.alreadyOpen = false; 22644 } 22645 22646 } 22647 22648 /** 22649 * Inserts and configures the staged Menu element into the DOM, positioning it, 22650 * and wiring up various interaction events 22651 */ 22652 function onShow(scope, element, opts) { 22653 sanitizeAndConfigure(opts); 22654 22655 // Wire up theming on our menu element 22656 $mdTheming.inherit(opts.menuContentEl, opts.target); 22657 22658 // Register various listeners to move menu on resize/orientation change 22659 opts.cleanupResizing = startRepositioningOnResize(); 22660 opts.hideBackdrop = showBackdrop(scope, element, opts); 22661 22662 // Return the promise for when our menu is done animating in 22663 return showMenu() 22664 .then(function(response) { 22665 opts.alreadyOpen = true; 22666 opts.cleanupInteraction = activateInteraction(); 22667 return response; 22668 }); 22669 22670 /** 22671 * Place the menu into the DOM and call positioning related functions 22672 */ 22673 function showMenu() { 22674 opts.parent.append(element); 22675 element[0].style.display = ''; 22676 22677 return $q(function(resolve) { 22678 var position = calculateMenuPosition(element, opts); 22679 22680 element.removeClass('md-leave'); 22681 22682 // Animate the menu scaling, and opacity [from its position origin (default == top-left)] 22683 // to normal scale. 22684 $animateCss(element, { 22685 addClass: 'md-active', 22686 from: animator.toCss(position), 22687 to: animator.toCss({transform: ''}) 22688 }) 22689 .start() 22690 .then(resolve); 22691 22692 }); 22693 } 22694 22695 /** 22696 * Check for valid opts and set some sane defaults 22697 */ 22698 function sanitizeAndConfigure() { 22699 if (!opts.target) { 22700 throw Error( 22701 '$mdMenu.show() expected a target to animate from in options.target' 22702 ); 22703 } 22704 angular.extend(opts, { 22705 alreadyOpen: false, 22706 isRemoved: false, 22707 target: angular.element(opts.target), //make sure it's not a naked dom node 22708 parent: angular.element(opts.parent), 22709 menuContentEl: angular.element(element[0].querySelector('md-menu-content')) 22710 }); 22711 } 22712 22713 /** 22714 * Configure various resize listeners for screen changes 22715 */ 22716 function startRepositioningOnResize() { 22717 22718 var repositionMenu = (function(target, options) { 22719 return $$rAF.throttle(function() { 22720 if (opts.isRemoved) return; 22721 var position = calculateMenuPosition(target, options); 22722 22723 target.css(animator.toCss(position)); 22724 }); 22725 })(element, opts); 22726 22727 $window.addEventListener('resize', repositionMenu); 22728 $window.addEventListener('orientationchange', repositionMenu); 22729 22730 return function stopRepositioningOnResize() { 22731 22732 // Disable resizing handlers 22733 $window.removeEventListener('resize', repositionMenu); 22734 $window.removeEventListener('orientationchange', repositionMenu); 22735 22736 } 22737 } 22738 22739 /** 22740 * Activate interaction on the menu. Wire up keyboard listerns for 22741 * clicks, keypresses, backdrop closing, etc. 22742 */ 22743 function activateInteraction() { 22744 element.addClass('md-clickable'); 22745 22746 // close on backdrop click 22747 if (opts.backdrop) opts.backdrop.on('click', onBackdropClick); 22748 22749 // Wire up keyboard listeners. 22750 // - Close on escape, 22751 // - focus next item on down arrow, 22752 // - focus prev item on up 22753 opts.menuContentEl.on('keydown', onMenuKeyDown); 22754 opts.menuContentEl[0].addEventListener('click', captureClickListener, true); 22755 22756 // kick off initial focus in the menu on the first element 22757 var focusTarget = opts.menuContentEl[0].querySelector('[md-menu-focus-target]'); 22758 if ( !focusTarget ) { 22759 var firstChild = opts.menuContentEl[0].firstElementChild; 22760 22761 focusTarget = firstChild && (firstChild.querySelector('.md-button:not([disabled])') || firstChild.firstElementChild); 22762 } 22763 22764 focusTarget && focusTarget.focus(); 22765 22766 return function cleanupInteraction() { 22767 element.removeClass('md-clickable'); 22768 if (opts.backdrop) opts.backdrop.off('click', onBackdropClick); 22769 opts.menuContentEl.off('keydown', onMenuKeyDown); 22770 opts.menuContentEl[0].removeEventListener('click', captureClickListener, true); 22771 }; 22772 22773 // ************************************ 22774 // internal functions 22775 // ************************************ 22776 22777 function onMenuKeyDown(ev) { 22778 var handled; 22779 switch (ev.keyCode) { 22780 case $mdConstant.KEY_CODE.ESCAPE: 22781 opts.mdMenuCtrl.close(false, { closeAll: true }); 22782 handled = true; 22783 break; 22784 case $mdConstant.KEY_CODE.UP_ARROW: 22785 if (!focusMenuItem(ev, opts.menuContentEl, opts, -1) && !opts.nestLevel) { 22786 opts.mdMenuCtrl.triggerContainerProxy(ev); 22787 } 22788 handled = true; 22789 break; 22790 case $mdConstant.KEY_CODE.DOWN_ARROW: 22791 if (!focusMenuItem(ev, opts.menuContentEl, opts, 1) && !opts.nestLevel) { 22792 opts.mdMenuCtrl.triggerContainerProxy(ev); 22793 } 22794 handled = true; 22795 break; 22796 case $mdConstant.KEY_CODE.LEFT_ARROW: 22797 if (opts.nestLevel) { 22798 opts.mdMenuCtrl.close(); 22799 } else { 22800 opts.mdMenuCtrl.triggerContainerProxy(ev); 22801 } 22802 handled = true; 22803 break; 22804 case $mdConstant.KEY_CODE.RIGHT_ARROW: 22805 var parentMenu = $mdUtil.getClosest(ev.target, 'MD-MENU'); 22806 if (parentMenu && parentMenu != opts.parent[0]) { 22807 ev.target.click(); 22808 } else { 22809 opts.mdMenuCtrl.triggerContainerProxy(ev); 22810 } 22811 handled = true; 22812 break; 22813 } 22814 if (handled) { 22815 ev.preventDefault(); 22816 ev.stopImmediatePropagation(); 22817 } 22818 } 22819 22820 function onBackdropClick(e) { 22821 e.preventDefault(); 22822 e.stopPropagation(); 22823 scope.$apply(function() { 22824 opts.mdMenuCtrl.close(true, { closeAll: true }); 22825 }); 22826 } 22827 22828 // Close menu on menu item click, if said menu-item is not disabled 22829 function captureClickListener(e) { 22830 var target = e.target; 22831 // Traverse up the event until we get to the menuContentEl to see if 22832 // there is an ng-click and that the ng-click is not disabled 22833 do { 22834 if (target == opts.menuContentEl[0]) return; 22835 if ((hasAnyAttribute(target, ['ng-click', 'ng-href', 'ui-sref']) || 22836 target.nodeName == 'BUTTON' || target.nodeName == 'MD-BUTTON') && !hasAnyAttribute(target, ['md-prevent-menu-close'])) { 22837 var closestMenu = $mdUtil.getClosest(target, 'MD-MENU'); 22838 if (!target.hasAttribute('disabled') && (!closestMenu || closestMenu == opts.parent[0])) { 22839 close(); 22840 } 22841 break; 22842 } 22843 } while (target = target.parentNode) 22844 22845 function close() { 22846 scope.$apply(function() { 22847 opts.mdMenuCtrl.close(true, { closeAll: true }); 22848 }); 22849 } 22850 22851 function hasAnyAttribute(target, attrs) { 22852 if (!target) return false; 22853 for (var i = 0, attr; attr = attrs[i]; ++i) { 22854 var altForms = [attr, 'data-' + attr, 'x-' + attr]; 22855 for (var j = 0, rawAttr; rawAttr = altForms[j]; ++j) { 22856 if (target.hasAttribute(rawAttr)) { 22857 return true; 22858 } 22859 } 22860 } 22861 return false; 22862 } 22863 } 22864 22865 opts.menuContentEl[0].addEventListener('click', captureClickListener, true); 22866 22867 return function cleanupInteraction() { 22868 element.removeClass('md-clickable'); 22869 opts.menuContentEl.off('keydown'); 22870 opts.menuContentEl[0].removeEventListener('click', captureClickListener, true); 22871 }; 22872 } 22873 } 22874 22875 /** 22876 * Takes a keypress event and focuses the next/previous menu 22877 * item from the emitting element 22878 * @param {event} e - The origin keypress event 22879 * @param {angular.element} menuEl - The menu element 22880 * @param {object} opts - The interim element options for the mdMenu 22881 * @param {number} direction - The direction to move in (+1 = next, -1 = prev) 22882 */ 22883 function focusMenuItem(e, menuEl, opts, direction) { 22884 var currentItem = $mdUtil.getClosest(e.target, 'MD-MENU-ITEM'); 22885 22886 var items = $mdUtil.nodesToArray(menuEl[0].children); 22887 var currentIndex = items.indexOf(currentItem); 22888 22889 // Traverse through our elements in the specified direction (+/-1) and try to 22890 // focus them until we find one that accepts focus 22891 var didFocus; 22892 for (var i = currentIndex + direction; i >= 0 && i < items.length; i = i + direction) { 22893 var focusTarget = items[i].querySelector('.md-button'); 22894 didFocus = attemptFocus(focusTarget); 22895 if (didFocus) { 22896 break; 22897 } 22898 } 22899 return didFocus; 22900 } 22901 22902 /** 22903 * Attempts to focus an element. Checks whether that element is the currently 22904 * focused element after attempting. 22905 * @param {HTMLElement} el - the element to attempt focus on 22906 * @returns {bool} - whether the element was successfully focused 22907 */ 22908 function attemptFocus(el) { 22909 if (el && el.getAttribute('tabindex') != -1) { 22910 el.focus(); 22911 return ($document[0].activeElement == el); 22912 } 22913 } 22914 22915 /** 22916 * Use browser to remove this element without triggering a $destroy event 22917 */ 22918 function detachElement(element, opts) { 22919 if (!opts.preserveElement) { 22920 if (toNode(element).parentNode === toNode(opts.parent)) { 22921 toNode(opts.parent).removeChild(toNode(element)); 22922 } 22923 } else { 22924 toNode(element).style.display = 'none'; 22925 } 22926 } 22927 22928 /** 22929 * Computes menu position and sets the style on the menu container 22930 * @param {HTMLElement} el - the menu container element 22931 * @param {object} opts - the interim element options object 22932 */ 22933 function calculateMenuPosition(el, opts) { 22934 22935 var containerNode = el[0], 22936 openMenuNode = el[0].firstElementChild, 22937 openMenuNodeRect = openMenuNode.getBoundingClientRect(), 22938 boundryNode = $document[0].body, 22939 boundryNodeRect = boundryNode.getBoundingClientRect(); 22940 22941 var menuStyle = $window.getComputedStyle(openMenuNode); 22942 22943 var originNode = opts.target[0].querySelector('[md-menu-origin]') || opts.target[0], 22944 originNodeRect = originNode.getBoundingClientRect(); 22945 22946 var bounds = { 22947 left: boundryNodeRect.left + MENU_EDGE_MARGIN, 22948 top: Math.max(boundryNodeRect.top, 0) + MENU_EDGE_MARGIN, 22949 bottom: Math.max(boundryNodeRect.bottom, Math.max(boundryNodeRect.top, 0) + boundryNodeRect.height) - MENU_EDGE_MARGIN, 22950 right: boundryNodeRect.right - MENU_EDGE_MARGIN 22951 }; 22952 22953 var alignTarget, alignTargetRect = { top:0, left : 0, right:0, bottom:0 }, existingOffsets = { top:0, left : 0, right:0, bottom:0 }; 22954 var positionMode = opts.mdMenuCtrl.positionMode(); 22955 22956 if (positionMode.top == 'target' || positionMode.left == 'target' || positionMode.left == 'target-right') { 22957 alignTarget = firstVisibleChild(); 22958 if ( alignTarget ) { 22959 // TODO: Allow centering on an arbitrary node, for now center on first menu-item's child 22960 alignTarget = alignTarget.firstElementChild || alignTarget; 22961 alignTarget = alignTarget.querySelector('[md-menu-align-target]') || alignTarget; 22962 alignTargetRect = alignTarget.getBoundingClientRect(); 22963 22964 existingOffsets = { 22965 top: parseFloat(containerNode.style.top || 0), 22966 left: parseFloat(containerNode.style.left || 0) 22967 }; 22968 } 22969 } 22970 22971 var position = {}; 22972 var transformOrigin = 'top '; 22973 22974 switch (positionMode.top) { 22975 case 'target': 22976 position.top = existingOffsets.top + originNodeRect.top - alignTargetRect.top; 22977 break; 22978 case 'cascade': 22979 position.top = originNodeRect.top - parseFloat(menuStyle.paddingTop) - originNode.style.top; 22980 break; 22981 case 'bottom': 22982 position.top = originNodeRect.top + originNodeRect.height; 22983 break; 22984 default: 22985 throw new Error('Invalid target mode "' + positionMode.top + '" specified for md-menu on Y axis.'); 22986 } 22987 22988 switch (positionMode.left) { 22989 case 'target': 22990 position.left = existingOffsets.left + originNodeRect.left - alignTargetRect.left; 22991 transformOrigin += 'left'; 22992 break; 22993 case 'target-right': 22994 position.left = originNodeRect.right - openMenuNodeRect.width + (openMenuNodeRect.right - alignTargetRect.right); 22995 transformOrigin += 'right'; 22996 break; 22997 case 'cascade': 22998 var willFitRight = (originNodeRect.right + openMenuNodeRect.width) < bounds.right; 22999 position.left = willFitRight ? originNodeRect.right - originNode.style.left : originNodeRect.left - originNode.style.left - openMenuNodeRect.width; 23000 transformOrigin += willFitRight ? 'left' : 'right'; 23001 break; 23002 case 'left': 23003 position.left = originNodeRect.left; 23004 transformOrigin += 'left'; 23005 break; 23006 default: 23007 throw new Error('Invalid target mode "' + positionMode.left + '" specified for md-menu on X axis.'); 23008 } 23009 23010 var offsets = opts.mdMenuCtrl.offsets(); 23011 position.top += offsets.top; 23012 position.left += offsets.left; 23013 23014 clamp(position); 23015 23016 var scaleX = Math.round(100 * Math.min(originNodeRect.width / containerNode.offsetWidth, 1.0)) / 100; 23017 var scaleY = Math.round(100 * Math.min(originNodeRect.height / containerNode.offsetHeight, 1.0)) / 100; 23018 23019 return { 23020 top: Math.round(position.top), 23021 left: Math.round(position.left), 23022 // Animate a scale out if we aren't just repositioning 23023 transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : undefined, 23024 transformOrigin: transformOrigin 23025 }; 23026 23027 /** 23028 * Clamps the repositioning of the menu within the confines of 23029 * bounding element (often the screen/body) 23030 */ 23031 function clamp(pos) { 23032 pos.top = Math.max(Math.min(pos.top, bounds.bottom - containerNode.offsetHeight), bounds.top); 23033 pos.left = Math.max(Math.min(pos.left, bounds.right - containerNode.offsetWidth), bounds.left); 23034 } 23035 23036 /** 23037 * Gets the first visible child in the openMenuNode 23038 * Necessary incase menu nodes are being dynamically hidden 23039 */ 23040 function firstVisibleChild() { 23041 for (var i = 0; i < openMenuNode.children.length; ++i) { 23042 if ($window.getComputedStyle(openMenuNode.children[i]).display != 'none') { 23043 return openMenuNode.children[i]; 23044 } 23045 } 23046 } 23047 } 23048 } 23049 function toNode(el) { 23050 if (el instanceof angular.element) { 23051 el = el[0]; 23052 } 23053 return el; 23054 } 23055 } 23056 MenuProvider.$inject = ["$$interimElementProvider"]; 23057 23058 })(); 23059 (function(){ 23060 "use strict"; 23061 23062 23063 angular 23064 .module('material.components.menuBar') 23065 .controller('MenuBarController', MenuBarController); 23066 23067 var BOUND_MENU_METHODS = ['handleKeyDown', 'handleMenuHover', 'scheduleOpenHoveredMenu', 'cancelScheduledOpen']; 23068 23069 /** 23070 * @ngInject 23071 */ 23072 function MenuBarController($scope, $rootScope, $element, $attrs, $mdConstant, $document, $mdUtil, $timeout) { 23073 this.$element = $element; 23074 this.$attrs = $attrs; 23075 this.$mdConstant = $mdConstant; 23076 this.$mdUtil = $mdUtil; 23077 this.$document = $document; 23078 this.$scope = $scope; 23079 this.$rootScope = $rootScope; 23080 this.$timeout = $timeout; 23081 23082 var self = this; 23083 angular.forEach(BOUND_MENU_METHODS, function(methodName) { 23084 self[methodName] = angular.bind(self, self[methodName]); 23085 }); 23086 } 23087 MenuBarController.$inject = ["$scope", "$rootScope", "$element", "$attrs", "$mdConstant", "$document", "$mdUtil", "$timeout"]; 23088 23089 MenuBarController.prototype.init = function() { 23090 var $element = this.$element; 23091 var $mdUtil = this.$mdUtil; 23092 var $scope = this.$scope; 23093 23094 var self = this; 23095 var deregisterFns = []; 23096 $element.on('keydown', this.handleKeyDown); 23097 this.parentToolbar = $mdUtil.getClosest($element, 'MD-TOOLBAR'); 23098 23099 deregisterFns.push(this.$rootScope.$on('$mdMenuOpen', function(event, el) { 23100 if (self.getMenus().indexOf(el[0]) != -1) { 23101 $element[0].classList.add('md-open'); 23102 el[0].classList.add('md-open'); 23103 self.currentlyOpenMenu = el.controller('mdMenu'); 23104 self.currentlyOpenMenu.registerContainerProxy(self.handleKeyDown); 23105 self.enableOpenOnHover(); 23106 } 23107 })); 23108 23109 deregisterFns.push(this.$rootScope.$on('$mdMenuClose', function(event, el, opts) { 23110 var rootMenus = self.getMenus(); 23111 if (rootMenus.indexOf(el[0]) != -1) { 23112 $element[0].classList.remove('md-open'); 23113 el[0].classList.remove('md-open'); 23114 } 23115 23116 if ($element[0].contains(el[0])) { 23117 var parentMenu = el[0]; 23118 while (parentMenu && rootMenus.indexOf(parentMenu) == -1) { 23119 parentMenu = $mdUtil.getClosest(parentMenu, 'MD-MENU', true); 23120 } 23121 if (parentMenu) { 23122 if (!opts.skipFocus) parentMenu.querySelector('button:not([disabled])').focus(); 23123 self.currentlyOpenMenu = undefined; 23124 self.disableOpenOnHover(); 23125 self.setKeyboardMode(true); 23126 } 23127 } 23128 })); 23129 23130 $scope.$on('$destroy', function() { 23131 while (deregisterFns.length) { 23132 deregisterFns.shift()(); 23133 } 23134 }); 23135 23136 23137 this.setKeyboardMode(true); 23138 }; 23139 23140 MenuBarController.prototype.setKeyboardMode = function(enabled) { 23141 if (enabled) this.$element[0].classList.add('md-keyboard-mode'); 23142 else this.$element[0].classList.remove('md-keyboard-mode'); 23143 }; 23144 23145 MenuBarController.prototype.enableOpenOnHover = function() { 23146 if (this.openOnHoverEnabled) return; 23147 this.openOnHoverEnabled = true; 23148 23149 var parentToolbar; 23150 if (parentToolbar = this.parentToolbar) { 23151 parentToolbar.dataset.mdRestoreStyle = parentToolbar.getAttribute('style'); 23152 parentToolbar.style.position = 'relative'; 23153 parentToolbar.style.zIndex = 100; 23154 } 23155 angular 23156 .element(this.getMenus()) 23157 .on('mouseenter', this.handleMenuHover); 23158 }; 23159 23160 MenuBarController.prototype.handleMenuHover = function(e) { 23161 this.setKeyboardMode(false); 23162 if (this.openOnHoverEnabled) { 23163 this.scheduleOpenHoveredMenu(e); 23164 } 23165 }; 23166 23167 23168 MenuBarController.prototype.disableOpenOnHover = function() { 23169 if (!this.openOnHoverEnabled) return; 23170 this.openOnHoverEnabled = false; 23171 var parentToolbar; 23172 if (parentToolbar = this.parentToolbar) { 23173 parentToolbar.style.cssText = parentToolbar.dataset.mdRestoreStyle || ''; 23174 } 23175 angular 23176 .element(this.getMenus()) 23177 .off('mouseenter', this.handleMenuHover); 23178 }; 23179 23180 MenuBarController.prototype.scheduleOpenHoveredMenu = function(e) { 23181 var menuEl = angular.element(e.currentTarget); 23182 var menuCtrl = menuEl.controller('mdMenu'); 23183 this.setKeyboardMode(false); 23184 this.scheduleOpenMenu(menuCtrl); 23185 }; 23186 23187 MenuBarController.prototype.scheduleOpenMenu = function(menuCtrl) { 23188 var self = this; 23189 var $timeout = this.$timeout; 23190 if (menuCtrl != self.currentlyOpenMenu) { 23191 $timeout.cancel(self.pendingMenuOpen); 23192 self.pendingMenuOpen = $timeout(function() { 23193 self.pendingMenuOpen = undefined; 23194 if (self.currentlyOpenMenu) { 23195 self.currentlyOpenMenu.close(true, { closeAll: true }); 23196 } 23197 menuCtrl.open(); 23198 }, 200, false); 23199 } 23200 }; 23201 23202 MenuBarController.prototype.handleKeyDown = function(e) { 23203 var keyCodes = this.$mdConstant.KEY_CODE; 23204 var currentMenu = this.currentlyOpenMenu; 23205 var wasOpen = currentMenu && currentMenu.isOpen; 23206 this.setKeyboardMode(true); 23207 var handled, newMenu, newMenuCtrl; 23208 switch (e.keyCode) { 23209 case keyCodes.DOWN_ARROW: 23210 if (currentMenu) { 23211 currentMenu.focusMenuContainer(); 23212 } else { 23213 this.openFocusedMenu(); 23214 } 23215 handled = true; 23216 break; 23217 case keyCodes.UP_ARROW: 23218 currentMenu && currentMenu.close(); 23219 handled = true; 23220 break; 23221 case keyCodes.LEFT_ARROW: 23222 newMenu = this.focusMenu(-1); 23223 if (wasOpen) { 23224 newMenuCtrl = angular.element(newMenu).controller('mdMenu'); 23225 this.scheduleOpenMenu(newMenuCtrl); 23226 } 23227 handled = true; 23228 break; 23229 case keyCodes.RIGHT_ARROW: 23230 newMenu = this.focusMenu(+1); 23231 if (wasOpen) { 23232 newMenuCtrl = angular.element(newMenu).controller('mdMenu'); 23233 this.scheduleOpenMenu(newMenuCtrl); 23234 } 23235 handled = true; 23236 break; 23237 } 23238 if (handled) { 23239 e && e.preventDefault && e.preventDefault(); 23240 e && e.stopImmediatePropagation && e.stopImmediatePropagation(); 23241 } 23242 }; 23243 23244 MenuBarController.prototype.focusMenu = function(direction) { 23245 var menus = this.getMenus(); 23246 var focusedIndex = this.getFocusedMenuIndex(); 23247 23248 if (focusedIndex == -1) { focusedIndex = this.getOpenMenuIndex(); } 23249 23250 var changed = false; 23251 23252 if (focusedIndex == -1) { focusedIndex = 0; changed = true; } 23253 else if ( 23254 direction < 0 && focusedIndex > 0 || 23255 direction > 0 && focusedIndex < menus.length - direction 23256 ) { 23257 focusedIndex += direction; 23258 changed = true; 23259 } 23260 if (changed) { 23261 menus[focusedIndex].querySelector('button').focus(); 23262 return menus[focusedIndex]; 23263 } 23264 }; 23265 23266 MenuBarController.prototype.openFocusedMenu = function() { 23267 var menu = this.getFocusedMenu(); 23268 menu && angular.element(menu).controller('mdMenu').open(); 23269 }; 23270 23271 MenuBarController.prototype.getMenus = function() { 23272 var $element = this.$element; 23273 return this.$mdUtil.nodesToArray($element[0].children) 23274 .filter(function(el) { return el.nodeName == 'MD-MENU'; }); 23275 }; 23276 23277 MenuBarController.prototype.getFocusedMenu = function() { 23278 return this.getMenus()[this.getFocusedMenuIndex()]; 23279 }; 23280 23281 MenuBarController.prototype.getFocusedMenuIndex = function() { 23282 var $mdUtil = this.$mdUtil; 23283 var focusedEl = $mdUtil.getClosest( 23284 this.$document[0].activeElement, 23285 'MD-MENU' 23286 ); 23287 if (!focusedEl) return -1; 23288 23289 var focusedIndex = this.getMenus().indexOf(focusedEl); 23290 return focusedIndex; 23291 23292 }; 23293 23294 MenuBarController.prototype.getOpenMenuIndex = function() { 23295 var menus = this.getMenus(); 23296 for (var i = 0; i < menus.length; ++i) { 23297 if (menus[i].classList.contains('md-open')) return i; 23298 } 23299 return -1; 23300 }; 23301 23302 23303 23304 23305 23306 23307 23308 23309 23310 })(); 23311 (function(){ 23312 "use strict"; 23313 23314 /** 23315 * @ngdoc directive 23316 * @name mdMenuBar 23317 * @module material.components.menu-bar 23318 * @restrict E 23319 * @description 23320 * 23321 * Menu bars are containers that hold multiple menus. They change the behavior and appearance 23322 * of the `md-menu` directive to behave similar to an operating system provided menu. 23323 * 23324 * @usage 23325 * <hljs lang="html"> 23326 * <md-menu-bar> 23327 * <md-menu> 23328 * <button ng-click="$mdOpenMenu()"> 23329 * File 23330 * </button> 23331 * <md-menu-content> 23332 * <md-menu-item> 23333 * <md-button ng-click="ctrl.sampleAction('share', $event)"> 23334 * Share... 23335 * </md-button> 23336 * </md-menu-item> 23337 * <md-menu-divider></md-menu-divider> 23338 * <md-menu-item> 23339 * <md-menu-item> 23340 * <md-menu> 23341 * <md-button ng-click="$mdOpenMenu()">New</md-button> 23342 * <md-menu-content> 23343 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Document', $event)">Document</md-button></md-menu-item> 23344 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Spreadsheet', $event)">Spreadsheet</md-button></md-menu-item> 23345 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Presentation', $event)">Presentation</md-button></md-menu-item> 23346 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Form', $event)">Form</md-button></md-menu-item> 23347 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Drawing', $event)">Drawing</md-button></md-menu-item> 23348 * </md-menu-content> 23349 * </md-menu> 23350 * </md-menu-item> 23351 * </md-menu-content> 23352 * </md-menu> 23353 * </md-menu-bar> 23354 * </hljs> 23355 * 23356 * ## Menu Bar Controls 23357 * 23358 * You may place `md-menu-items` that function as controls within menu bars. 23359 * There are two modes that are exposed via the `type` attribute of the `md-menu-item`. 23360 * `type="checkbox"` will function as a boolean control for the `ng-model` attribute of the 23361 * `md-menu-item`. `type="radio"` will function like a radio button, setting the `ngModel` 23362 * to the `string` value of the `value` attribute. If you need non-string values, you can use 23363 * `ng-value` to provide an expression (this is similar to how angular's native `input[type=radio]` works. 23364 * 23365 * <hljs lang="html"> 23366 * <md-menu-bar> 23367 * <md-menu> 23368 * <button ng-click="$mdOpenMenu()"> 23369 * Sample Menu 23370 * </button> 23371 * <md-menu-content> 23372 * <md-menu-item type="checkbox" ng-model="settings.allowChanges">Allow changes</md-menu-item> 23373 * <md-menu-divider></md-menu-divider> 23374 * <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 1</md-menu-item> 23375 * <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 2</md-menu-item> 23376 * <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 3</md-menu-item> 23377 * </md-menu-content> 23378 * </md-menu> 23379 * </md-menu-bar> 23380 * </hljs> 23381 * 23382 * 23383 * ### Nesting Menus 23384 * 23385 * Menus may be nested within menu bars. This is commonly called cascading menus. 23386 * To nest a menu place the nested menu inside the content of the `md-menu-item`. 23387 * <hljs lang="html"> 23388 * <md-menu-item> 23389 * <md-menu> 23390 * <button ng-click="$mdOpenMenu()">New</md-button> 23391 * <md-menu-content> 23392 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Document', $event)">Document</md-button></md-menu-item> 23393 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Spreadsheet', $event)">Spreadsheet</md-button></md-menu-item> 23394 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Presentation', $event)">Presentation</md-button></md-menu-item> 23395 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Form', $event)">Form</md-button></md-menu-item> 23396 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Drawing', $event)">Drawing</md-button></md-menu-item> 23397 * </md-menu-content> 23398 * </md-menu> 23399 * </md-menu-item> 23400 * </hljs> 23401 * 23402 */ 23403 23404 angular 23405 .module('material.components.menuBar') 23406 .directive('mdMenuBar', MenuBarDirective); 23407 23408 /* @ngInject */ 23409 function MenuBarDirective($mdUtil, $mdTheming) { 23410 return { 23411 restrict: 'E', 23412 require: 'mdMenuBar', 23413 controller: 'MenuBarController', 23414 23415 compile: function compile(templateEl, templateAttrs) { 23416 if (!templateAttrs.ariaRole) { 23417 templateEl[0].setAttribute('role', 'menubar'); 23418 } 23419 angular.forEach(templateEl[0].children, function(menuEl) { 23420 if (menuEl.nodeName == 'MD-MENU') { 23421 if (!menuEl.hasAttribute('md-position-mode')) { 23422 menuEl.setAttribute('md-position-mode', 'left bottom'); 23423 menuEl.querySelector('button,a').setAttribute('role', 'menuitem'); 23424 } 23425 var contentEls = $mdUtil.nodesToArray(menuEl.querySelectorAll('md-menu-content')); 23426 angular.forEach(contentEls, function(contentEl) { 23427 contentEl.classList.add('md-menu-bar-menu'); 23428 contentEl.classList.add('md-dense'); 23429 if (!contentEl.hasAttribute('width')) { 23430 contentEl.setAttribute('width', 5); 23431 } 23432 }); 23433 } 23434 }); 23435 23436 return function postLink(scope, el, attrs, ctrl) { 23437 $mdTheming(scope, el); 23438 ctrl.init(); 23439 }; 23440 } 23441 }; 23442 23443 } 23444 MenuBarDirective.$inject = ["$mdUtil", "$mdTheming"]; 23445 23446 })(); 23447 (function(){ 23448 "use strict"; 23449 23450 23451 angular 23452 .module('material.components.menuBar') 23453 .directive('mdMenuDivider', MenuDividerDirective); 23454 23455 23456 function MenuDividerDirective() { 23457 return { 23458 restrict: 'E', 23459 compile: function(templateEl, templateAttrs) { 23460 if (!templateAttrs.role) { 23461 templateEl[0].setAttribute('role', 'separator'); 23462 } 23463 } 23464 }; 23465 } 23466 23467 })(); 23468 (function(){ 23469 "use strict"; 23470 23471 23472 angular 23473 .module('material.components.menuBar') 23474 .controller('MenuItemController', MenuItemController); 23475 23476 23477 /** 23478 * @ngInject 23479 */ 23480 function MenuItemController($scope, $element, $attrs) { 23481 this.$element = $element; 23482 this.$attrs = $attrs; 23483 this.$scope = $scope; 23484 } 23485 MenuItemController.$inject = ["$scope", "$element", "$attrs"]; 23486 23487 MenuItemController.prototype.init = function(ngModel) { 23488 var $element = this.$element; 23489 var $attrs = this.$attrs; 23490 23491 this.ngModel = ngModel; 23492 if ($attrs.type == 'checkbox' || $attrs.type == 'radio') { 23493 this.mode = $attrs.type; 23494 this.iconEl = $element[0].children[0]; 23495 this.buttonEl = $element[0].children[1]; 23496 if (ngModel) { 23497 // Clear ngAria set attributes 23498 this.initClickListeners(); 23499 } 23500 } 23501 }; 23502 23503 // ngAria auto sets attributes on a menu-item with a ngModel. 23504 // We don't want this because our content (buttons) get the focus 23505 // and set their own aria attributes appropritately. Having both 23506 // breaks NVDA / JAWS. This undeoes ngAria's attrs. 23507 MenuItemController.prototype.clearNgAria = function() { 23508 var el = this.$element[0]; 23509 var clearAttrs = ['role', 'tabindex', 'aria-invalid', 'aria-checked']; 23510 angular.forEach(clearAttrs, function(attr) { 23511 el.removeAttribute(attr); 23512 }); 23513 }; 23514 23515 MenuItemController.prototype.initClickListeners = function() { 23516 var self = this; 23517 var ngModel = this.ngModel; 23518 var $scope = this.$scope; 23519 var $attrs = this.$attrs; 23520 var $element = this.$element; 23521 var mode = this.mode; 23522 23523 this.handleClick = angular.bind(this, this.handleClick); 23524 23525 var icon = this.iconEl; 23526 var button = angular.element(this.buttonEl); 23527 var handleClick = this.handleClick; 23528 23529 $attrs.$observe('disabled', setDisabled); 23530 setDisabled($attrs.disabled); 23531 23532 ngModel.$render = function render() { 23533 self.clearNgAria(); 23534 if (isSelected()) { 23535 icon.style.display = ''; 23536 button.attr('aria-checked', 'true'); 23537 } else { 23538 icon.style.display = 'none'; 23539 button.attr('aria-checked', 'false'); 23540 } 23541 }; 23542 23543 $scope.$$postDigest(ngModel.$render); 23544 23545 function isSelected() { 23546 if (mode == 'radio') { 23547 var val = $attrs.ngValue ? $scope.$eval($attrs.ngValue) : $attrs.value; 23548 return ngModel.$modelValue == val; 23549 } else { 23550 return ngModel.$modelValue; 23551 } 23552 } 23553 23554 function setDisabled(disabled) { 23555 if (disabled) { 23556 button.off('click', handleClick); 23557 } else { 23558 button.on('click', handleClick); 23559 } 23560 } 23561 }; 23562 23563 MenuItemController.prototype.handleClick = function(e) { 23564 var mode = this.mode; 23565 var ngModel = this.ngModel; 23566 var $attrs = this.$attrs; 23567 var newVal; 23568 if (mode == 'checkbox') { 23569 newVal = !ngModel.$modelValue; 23570 } else if (mode == 'radio') { 23571 newVal = $attrs.ngValue ? this.$scope.$eval($attrs.ngValue) : $attrs.value; 23572 } 23573 ngModel.$setViewValue(newVal); 23574 ngModel.$render(); 23575 }; 23576 23577 })(); 23578 (function(){ 23579 "use strict"; 23580 23581 23582 angular 23583 .module('material.components.menuBar') 23584 .directive('mdMenuItem', MenuItemDirective); 23585 23586 /* @ngInject */ 23587 function MenuItemDirective() { 23588 return { 23589 require: ['mdMenuItem', '?ngModel'], 23590 priority: 210, // ensure that our post link runs after ngAria 23591 compile: function(templateEl, templateAttrs) { 23592 if (templateAttrs.type == 'checkbox' || templateAttrs.type == 'radio') { 23593 var text = templateEl[0].textContent; 23594 var buttonEl = angular.element('<md-button type="button"></md-button>'); 23595 buttonEl.html(text); 23596 buttonEl.attr('tabindex', '0'); 23597 23598 templateEl.html(''); 23599 templateEl.append(angular.element('<md-icon md-svg-icon="check"></md-icon>')); 23600 templateEl.append(buttonEl); 23601 templateEl[0].classList.add('md-indent'); 23602 23603 setDefault('role', (templateAttrs.type == 'checkbox') ? 'menuitemcheckbox' : 'menuitemradio', buttonEl); 23604 angular.forEach(['ng-disabled'], moveAttrToButton); 23605 23606 } else { 23607 setDefault('role', 'menuitem', templateEl[0].querySelector('md-button,button,a')); 23608 } 23609 23610 23611 return function(scope, el, attrs, ctrls) { 23612 var ctrl = ctrls[0]; 23613 var ngModel = ctrls[1]; 23614 ctrl.init(ngModel); 23615 }; 23616 23617 function setDefault(attr, val, el) { 23618 el = el || templateEl; 23619 if (el instanceof angular.element) { 23620 el = el[0]; 23621 } 23622 if (!el.hasAttribute(attr)) { 23623 el.setAttribute(attr, val); 23624 } 23625 } 23626 23627 function moveAttrToButton(attr) { 23628 if (templateEl[0].hasAttribute(attr)) { 23629 var val = templateEl[0].getAttribute(attr); 23630 buttonEl[0].setAttribute(attr, val); 23631 templateEl[0].removeAttribute(attr); 23632 } 23633 } 23634 }, 23635 controller: 'MenuItemController' 23636 }; 23637 } 23638 23639 })(); 23640 (function(){ 23641 "use strict"; 23642 23643 /** 23644 * @ngdoc directive 23645 * @name mdTab 23646 * @module material.components.tabs 23647 * 23648 * @restrict E 23649 * 23650 * @description 23651 * Use the `<md-tab>` a nested directive used within `<md-tabs>` to specify a tab with a **label** and optional *view content*. 23652 * 23653 * If the `label` attribute is not specified, then an optional `<md-tab-label>` tag can be used to specify more 23654 * complex tab header markup. If neither the **label** nor the **md-tab-label** are specified, then the nested 23655 * markup of the `<md-tab>` is used as the tab header markup. 23656 * 23657 * Please note that if you use `<md-tab-label>`, your content **MUST** be wrapped in the `<md-tab-body>` tag. This 23658 * is to define a clear separation between the tab content and the tab label. 23659 * 23660 * This container is used by the TabsController to show/hide the active tab's content view. This synchronization is 23661 * automatically managed by the internal TabsController whenever the tab selection changes. Selection changes can 23662 * be initiated via data binding changes, programmatic invocation, or user gestures. 23663 * 23664 * @param {string=} label Optional attribute to specify a simple string as the tab label 23665 * @param {boolean=} ng-disabled If present and expression evaluates to truthy, disabled tab selection. 23666 * @param {expression=} md-on-deselect Expression to be evaluated after the tab has been de-selected. 23667 * @param {expression=} md-on-select Expression to be evaluated after the tab has been selected. 23668 * @param {boolean=} md-active When true, sets the active tab. Note: There can only be one active tab at a time. 23669 * 23670 * 23671 * @usage 23672 * 23673 * <hljs lang="html"> 23674 * <md-tab label="" ng-disabled md-on-select="" md-on-deselect="" > 23675 * <h3>My Tab content</h3> 23676 * </md-tab> 23677 * 23678 * <md-tab > 23679 * <md-tab-label> 23680 * <h3>My Tab content</h3> 23681 * </md-tab-label> 23682 * <md-tab-body> 23683 * <p> 23684 * Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, 23685 * totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae 23686 * dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, 23687 * sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. 23688 * </p> 23689 * </md-tab-body> 23690 * </md-tab> 23691 * </hljs> 23692 * 23693 */ 23694 angular 23695 .module('material.components.tabs') 23696 .directive('mdTab', MdTab); 23697 23698 function MdTab () { 23699 return { 23700 require: '^?mdTabs', 23701 terminal: true, 23702 compile: function (element, attr) { 23703 var label = firstChild(element, 'md-tab-label'), 23704 body = firstChild(element, 'md-tab-body'); 23705 23706 if (label.length == 0) { 23707 label = angular.element('<md-tab-label></md-tab-label>'); 23708 if (attr.label) label.text(attr.label); 23709 else label.append(element.contents()); 23710 23711 if (body.length == 0) { 23712 var contents = element.contents().detach(); 23713 body = angular.element('<md-tab-body></md-tab-body>'); 23714 body.append(contents); 23715 } 23716 } 23717 23718 element.append(label); 23719 if (body.html()) element.append(body); 23720 23721 return postLink; 23722 }, 23723 scope: { 23724 active: '=?mdActive', 23725 disabled: '=?ngDisabled', 23726 select: '&?mdOnSelect', 23727 deselect: '&?mdOnDeselect' 23728 } 23729 }; 23730 23731 function postLink (scope, element, attr, ctrl) { 23732 if (!ctrl) return; 23733 var index = ctrl.getTabElementIndex(element), 23734 body = firstChild(element, 'md-tab-body').remove(), 23735 label = firstChild(element, 'md-tab-label').remove(), 23736 data = ctrl.insertTab({ 23737 scope: scope, 23738 parent: scope.$parent, 23739 index: index, 23740 element: element, 23741 template: body.html(), 23742 label: label.html() 23743 }, index); 23744 23745 scope.select = scope.select || angular.noop; 23746 scope.deselect = scope.deselect || angular.noop; 23747 23748 scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex(), true); }); 23749 scope.$watch('disabled', function () { ctrl.refreshIndex(); }); 23750 scope.$watch( 23751 function () { 23752 return ctrl.getTabElementIndex(element); 23753 }, 23754 function (newIndex) { 23755 data.index = newIndex; 23756 ctrl.updateTabOrder(); 23757 } 23758 ); 23759 scope.$on('$destroy', function () { ctrl.removeTab(data); }); 23760 } 23761 23762 function firstChild (element, tagName) { 23763 var children = element[0].children; 23764 for (var i = 0, len = children.length; i < len; i++) { 23765 var child = children[i]; 23766 if (child.tagName === tagName.toUpperCase()) return angular.element(child); 23767 } 23768 return angular.element(); 23769 } 23770 } 23771 23772 })(); 23773 (function(){ 23774 "use strict"; 23775 23776 angular 23777 .module('material.components.tabs') 23778 .directive('mdTabItem', MdTabItem); 23779 23780 function MdTabItem () { 23781 return { 23782 require: '^?mdTabs', 23783 link: function link (scope, element, attr, ctrl) { 23784 if (!ctrl) return; 23785 ctrl.attachRipple(scope, element); 23786 } 23787 }; 23788 } 23789 23790 })(); 23791 (function(){ 23792 "use strict"; 23793 23794 angular 23795 .module('material.components.tabs') 23796 .directive('mdTabLabel', MdTabLabel); 23797 23798 function MdTabLabel () { 23799 return { terminal: true }; 23800 } 23801 23802 23803 })(); 23804 (function(){ 23805 "use strict"; 23806 23807 angular.module('material.components.tabs') 23808 .directive('mdTabScroll', MdTabScroll); 23809 23810 function MdTabScroll ($parse) { 23811 return { 23812 restrict: 'A', 23813 compile: function ($element, attr) { 23814 var fn = $parse(attr.mdTabScroll, null, true); 23815 return function ngEventHandler (scope, element) { 23816 element.on('mousewheel', function (event) { 23817 scope.$apply(function () { fn(scope, { $event: event }); }); 23818 }); 23819 }; 23820 } 23821 } 23822 } 23823 MdTabScroll.$inject = ["$parse"]; 23824 23825 })(); 23826 (function(){ 23827 "use strict"; 23828 23829 angular 23830 .module('material.components.tabs') 23831 .controller('MdTabsController', MdTabsController); 23832 23833 /** 23834 * @ngInject 23835 */ 23836 function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipple, 23837 $mdUtil, $animateCss, $attrs, $compile, $mdTheming) { 23838 // define private properties 23839 var ctrl = this, 23840 locked = false, 23841 elements = getElements(), 23842 queue = [], 23843 destroyed = false, 23844 loaded = false; 23845 23846 // define one-way bindings 23847 defineOneWayBinding('stretchTabs', handleStretchTabs); 23848 23849 // define public properties with change handlers 23850 defineProperty('focusIndex', handleFocusIndexChange, ctrl.selectedIndex || 0); 23851 defineProperty('offsetLeft', handleOffsetChange, 0); 23852 defineProperty('hasContent', handleHasContent, false); 23853 defineProperty('maxTabWidth', handleMaxTabWidth, getMaxTabWidth()); 23854 defineProperty('shouldPaginate', handleShouldPaginate, false); 23855 23856 // define boolean attributes 23857 defineBooleanAttribute('noInkBar', handleInkBar); 23858 defineBooleanAttribute('dynamicHeight', handleDynamicHeight); 23859 defineBooleanAttribute('noPagination'); 23860 defineBooleanAttribute('swipeContent'); 23861 defineBooleanAttribute('noDisconnect'); 23862 defineBooleanAttribute('autoselect'); 23863 defineBooleanAttribute('noSelectClick'); 23864 defineBooleanAttribute('centerTabs', handleCenterTabs, false); 23865 defineBooleanAttribute('enableDisconnect'); 23866 23867 // define public properties 23868 ctrl.scope = $scope; 23869 ctrl.parent = $scope.$parent; 23870 ctrl.tabs = []; 23871 ctrl.lastSelectedIndex = null; 23872 ctrl.hasFocus = false; 23873 ctrl.lastClick = true; 23874 ctrl.shouldCenterTabs = shouldCenterTabs(); 23875 23876 // define public methods 23877 ctrl.updatePagination = $mdUtil.debounce(updatePagination, 100); 23878 ctrl.redirectFocus = redirectFocus; 23879 ctrl.attachRipple = attachRipple; 23880 ctrl.insertTab = insertTab; 23881 ctrl.removeTab = removeTab; 23882 ctrl.select = select; 23883 ctrl.scroll = scroll; 23884 ctrl.nextPage = nextPage; 23885 ctrl.previousPage = previousPage; 23886 ctrl.keydown = keydown; 23887 ctrl.canPageForward = canPageForward; 23888 ctrl.canPageBack = canPageBack; 23889 ctrl.refreshIndex = refreshIndex; 23890 ctrl.incrementIndex = incrementIndex; 23891 ctrl.getTabElementIndex = getTabElementIndex; 23892 ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100); 23893 ctrl.updateTabOrder = $mdUtil.debounce(updateTabOrder, 100); 23894 23895 init(); 23896 23897 /** 23898 * Perform initialization for the controller, setup events and watcher(s) 23899 */ 23900 function init () { 23901 ctrl.selectedIndex = ctrl.selectedIndex || 0; 23902 compileTemplate(); 23903 configureWatchers(); 23904 bindEvents(); 23905 $mdTheming($element); 23906 $mdUtil.nextTick(function () { 23907 updateHeightFromContent(); 23908 adjustOffset(); 23909 updateInkBarStyles(); 23910 ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select(); 23911 loaded = true; 23912 updatePagination(); 23913 }); 23914 } 23915 23916 /** 23917 * Compiles the template provided by the user. This is passed as an attribute from the tabs 23918 * directive's template function. 23919 */ 23920 function compileTemplate () { 23921 var template = $attrs.$mdTabsTemplate, 23922 element = angular.element(elements.data); 23923 element.html(template); 23924 $compile(element.contents())(ctrl.parent); 23925 delete $attrs.$mdTabsTemplate; 23926 } 23927 23928 /** 23929 * Binds events used by the tabs component. 23930 */ 23931 function bindEvents () { 23932 angular.element($window).on('resize', handleWindowResize); 23933 $scope.$on('$destroy', cleanup); 23934 } 23935 23936 /** 23937 * Configure watcher(s) used by Tabs 23938 */ 23939 function configureWatchers () { 23940 $scope.$watch('$mdTabsCtrl.selectedIndex', handleSelectedIndexChange); 23941 } 23942 23943 /** 23944 * Creates a one-way binding manually rather than relying on Angular's isolated scope 23945 * @param key 23946 * @param handler 23947 */ 23948 function defineOneWayBinding (key, handler) { 23949 var attr = $attrs.$normalize('md-' + key); 23950 if (handler) defineProperty(key, handler); 23951 $attrs.$observe(attr, function (newValue) { ctrl[ key ] = newValue; }); 23952 } 23953 23954 /** 23955 * Defines boolean attributes with default value set to true. (ie. md-stretch-tabs with no value 23956 * will be treated as being truthy) 23957 * @param key 23958 * @param handler 23959 */ 23960 function defineBooleanAttribute (key, handler) { 23961 var attr = $attrs.$normalize('md-' + key); 23962 if (handler) defineProperty(key, handler); 23963 if ($attrs.hasOwnProperty(attr)) updateValue($attrs[attr]); 23964 $attrs.$observe(attr, updateValue); 23965 function updateValue (newValue) { 23966 ctrl[ key ] = newValue !== 'false'; 23967 } 23968 } 23969 23970 /** 23971 * Remove any events defined by this controller 23972 */ 23973 function cleanup () { 23974 destroyed = true; 23975 angular.element($window).off('resize', handleWindowResize); 23976 } 23977 23978 // Change handlers 23979 23980 /** 23981 * Toggles stretch tabs class and updates inkbar when tab stretching changes 23982 * @param stretchTabs 23983 */ 23984 function handleStretchTabs (stretchTabs) { 23985 angular.element(elements.wrapper).toggleClass('md-stretch-tabs', shouldStretchTabs()); 23986 updateInkBarStyles(); 23987 } 23988 23989 function handleCenterTabs (newValue) { 23990 ctrl.shouldCenterTabs = shouldCenterTabs(); 23991 } 23992 23993 function handleMaxTabWidth (newWidth, oldWidth) { 23994 if (newWidth !== oldWidth) { 23995 angular.forEach(elements.tabs, function(tab) { 23996 tab.style.maxWidth = newWidth + 'px'; 23997 }); 23998 $mdUtil.nextTick(ctrl.updateInkBarStyles); 23999 } 24000 } 24001 24002 function handleShouldPaginate (newValue, oldValue) { 24003 if (newValue !== oldValue) { 24004 ctrl.maxTabWidth = getMaxTabWidth(); 24005 ctrl.shouldCenterTabs = shouldCenterTabs(); 24006 $mdUtil.nextTick(function () { 24007 ctrl.maxTabWidth = getMaxTabWidth(); 24008 adjustOffset(ctrl.selectedIndex); 24009 }); 24010 } 24011 } 24012 24013 /** 24014 * Add/remove the `md-no-tab-content` class depending on `ctrl.hasContent` 24015 * @param hasContent 24016 */ 24017 function handleHasContent (hasContent) { 24018 $element[ hasContent ? 'removeClass' : 'addClass' ]('md-no-tab-content'); 24019 } 24020 24021 /** 24022 * Apply ctrl.offsetLeft to the paging element when it changes 24023 * @param left 24024 */ 24025 function handleOffsetChange (left) { 24026 var newValue = ctrl.shouldCenterTabs ? '' : '-' + left + 'px'; 24027 angular.element(elements.paging).css($mdConstant.CSS.TRANSFORM, 'translate3d(' + newValue + ', 0, 0)'); 24028 $scope.$broadcast('$mdTabsPaginationChanged'); 24029 } 24030 24031 /** 24032 * Update the UI whenever `ctrl.focusIndex` is updated 24033 * @param newIndex 24034 * @param oldIndex 24035 */ 24036 function handleFocusIndexChange (newIndex, oldIndex) { 24037 if (newIndex === oldIndex) return; 24038 if (!elements.tabs[ newIndex ]) return; 24039 adjustOffset(); 24040 redirectFocus(); 24041 } 24042 24043 /** 24044 * Update the UI whenever the selected index changes. Calls user-defined select/deselect methods. 24045 * @param newValue 24046 * @param oldValue 24047 */ 24048 function handleSelectedIndexChange (newValue, oldValue) { 24049 if (newValue === oldValue) return; 24050 24051 ctrl.selectedIndex = getNearestSafeIndex(newValue); 24052 ctrl.lastSelectedIndex = oldValue; 24053 ctrl.updateInkBarStyles(); 24054 updateHeightFromContent(); 24055 adjustOffset(newValue); 24056 $scope.$broadcast('$mdTabsChanged'); 24057 ctrl.tabs[ oldValue ] && ctrl.tabs[ oldValue ].scope.deselect(); 24058 ctrl.tabs[ newValue ] && ctrl.tabs[ newValue ].scope.select(); 24059 } 24060 24061 function getTabElementIndex(tabEl){ 24062 var tabs = $element[0].getElementsByTagName('md-tab'); 24063 return Array.prototype.indexOf.call(tabs, tabEl[0]); 24064 } 24065 24066 /** 24067 * Queues up a call to `handleWindowResize` when a resize occurs while the tabs component is 24068 * hidden. 24069 */ 24070 function handleResizeWhenVisible () { 24071 // if there is already a watcher waiting for resize, do nothing 24072 if (handleResizeWhenVisible.watcher) return; 24073 // otherwise, we will abuse the $watch function to check for visible 24074 handleResizeWhenVisible.watcher = $scope.$watch(function () { 24075 // since we are checking for DOM size, we use $mdUtil.nextTick() to wait for after the DOM updates 24076 $mdUtil.nextTick(function () { 24077 // if the watcher has already run (ie. multiple digests in one cycle), do nothing 24078 if (!handleResizeWhenVisible.watcher) return; 24079 24080 if ($element.prop('offsetParent')) { 24081 handleResizeWhenVisible.watcher(); 24082 handleResizeWhenVisible.watcher = null; 24083 24084 handleWindowResize(); 24085 } 24086 }, false); 24087 }); 24088 } 24089 24090 // Event handlers / actions 24091 24092 /** 24093 * Handle user keyboard interactions 24094 * @param event 24095 */ 24096 function keydown (event) { 24097 switch (event.keyCode) { 24098 case $mdConstant.KEY_CODE.LEFT_ARROW: 24099 event.preventDefault(); 24100 incrementIndex(-1, true); 24101 break; 24102 case $mdConstant.KEY_CODE.RIGHT_ARROW: 24103 event.preventDefault(); 24104 incrementIndex(1, true); 24105 break; 24106 case $mdConstant.KEY_CODE.SPACE: 24107 case $mdConstant.KEY_CODE.ENTER: 24108 event.preventDefault(); 24109 if (!locked) ctrl.selectedIndex = ctrl.focusIndex; 24110 break; 24111 } 24112 ctrl.lastClick = false; 24113 } 24114 24115 /** 24116 * Update the selected index. Triggers a click event on the original `md-tab` element in order 24117 * to fire user-added click events if canSkipClick or `md-no-select-click` are false. 24118 * @param index 24119 * @param canSkipClick Optionally allow not firing the click event if `md-no-select-click` is also true. 24120 */ 24121 function select (index, canSkipClick) { 24122 if (!locked) ctrl.focusIndex = ctrl.selectedIndex = index; 24123 ctrl.lastClick = true; 24124 // skip the click event if noSelectClick is enabled 24125 if (canSkipClick && ctrl.noSelectClick) return; 24126 // nextTick is required to prevent errors in user-defined click events 24127 $mdUtil.nextTick(function () { 24128 ctrl.tabs[ index ].element.triggerHandler('click'); 24129 }, false); 24130 } 24131 24132 /** 24133 * When pagination is on, this makes sure the selected index is in view. 24134 * @param event 24135 */ 24136 function scroll (event) { 24137 if (!ctrl.shouldPaginate) return; 24138 event.preventDefault(); 24139 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta); 24140 } 24141 24142 /** 24143 * Slides the tabs over approximately one page forward. 24144 */ 24145 function nextPage () { 24146 var viewportWidth = elements.canvas.clientWidth, 24147 totalWidth = viewportWidth + ctrl.offsetLeft, 24148 i, tab; 24149 for (i = 0; i < elements.tabs.length; i++) { 24150 tab = elements.tabs[ i ]; 24151 if (tab.offsetLeft + tab.offsetWidth > totalWidth) break; 24152 } 24153 ctrl.offsetLeft = fixOffset(tab.offsetLeft); 24154 } 24155 24156 /** 24157 * Slides the tabs over approximately one page backward. 24158 */ 24159 function previousPage () { 24160 var i, tab; 24161 for (i = 0; i < elements.tabs.length; i++) { 24162 tab = elements.tabs[ i ]; 24163 if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break; 24164 } 24165 ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth); 24166 } 24167 24168 /** 24169 * Update size calculations when the window is resized. 24170 */ 24171 function handleWindowResize () { 24172 ctrl.lastSelectedIndex = ctrl.selectedIndex; 24173 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft); 24174 $mdUtil.nextTick(function () { 24175 ctrl.updateInkBarStyles(); 24176 updatePagination(); 24177 }); 24178 } 24179 24180 function handleInkBar (hide) { 24181 angular.element(elements.inkBar).toggleClass('ng-hide', hide); 24182 } 24183 24184 /** 24185 * Toggle dynamic height class when value changes 24186 * @param value 24187 */ 24188 function handleDynamicHeight (value) { 24189 $element.toggleClass('md-dynamic-height', value); 24190 } 24191 24192 /** 24193 * Remove a tab from the data and select the nearest valid tab. 24194 * @param tabData 24195 */ 24196 function removeTab (tabData) { 24197 if (destroyed) return; 24198 var selectedIndex = ctrl.selectedIndex, 24199 tab = ctrl.tabs.splice(tabData.getIndex(), 1)[ 0 ]; 24200 refreshIndex(); 24201 // when removing a tab, if the selected index did not change, we have to manually trigger the 24202 // tab select/deselect events 24203 if (ctrl.selectedIndex === selectedIndex) { 24204 tab.scope.deselect(); 24205 ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select(); 24206 } 24207 $mdUtil.nextTick(function () { 24208 updatePagination(); 24209 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft); 24210 }); 24211 } 24212 24213 /** 24214 * Create an entry in the tabs array for a new tab at the specified index. 24215 * @param tabData 24216 * @param index 24217 * @returns {*} 24218 */ 24219 function insertTab (tabData, index) { 24220 var hasLoaded = loaded; 24221 var proto = { 24222 getIndex: function () { return ctrl.tabs.indexOf(tab); }, 24223 isActive: function () { return this.getIndex() === ctrl.selectedIndex; }, 24224 isLeft: function () { return this.getIndex() < ctrl.selectedIndex; }, 24225 isRight: function () { return this.getIndex() > ctrl.selectedIndex; }, 24226 shouldRender: function () { return !ctrl.noDisconnect || this.isActive(); }, 24227 hasFocus: function () { 24228 return !ctrl.lastClick 24229 && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex; 24230 }, 24231 id: $mdUtil.nextUid() 24232 }, 24233 tab = angular.extend(proto, tabData); 24234 if (angular.isDefined(index)) { 24235 ctrl.tabs.splice(index, 0, tab); 24236 } else { 24237 ctrl.tabs.push(tab); 24238 } 24239 processQueue(); 24240 updateHasContent(); 24241 $mdUtil.nextTick(function () { 24242 updatePagination(); 24243 // if autoselect is enabled, select the newly added tab 24244 if (hasLoaded && ctrl.autoselect) $mdUtil.nextTick(function () { 24245 $mdUtil.nextTick(function () { select(ctrl.tabs.indexOf(tab)); }); 24246 }); 24247 }); 24248 return tab; 24249 } 24250 24251 // Getter methods 24252 24253 /** 24254 * Gathers references to all of the DOM elements used by this controller. 24255 * @returns {{}} 24256 */ 24257 function getElements () { 24258 var elements = {}; 24259 24260 // gather tab bar elements 24261 elements.wrapper = $element[ 0 ].getElementsByTagName('md-tabs-wrapper')[ 0 ]; 24262 elements.data = $element[ 0 ].getElementsByTagName('md-tab-data')[ 0 ]; 24263 elements.canvas = elements.wrapper.getElementsByTagName('md-tabs-canvas')[ 0 ]; 24264 elements.paging = elements.canvas.getElementsByTagName('md-pagination-wrapper')[ 0 ]; 24265 elements.tabs = elements.paging.getElementsByTagName('md-tab-item'); 24266 elements.dummies = elements.canvas.getElementsByTagName('md-dummy-tab'); 24267 elements.inkBar = elements.paging.getElementsByTagName('md-ink-bar')[ 0 ]; 24268 24269 // gather tab content elements 24270 elements.contentsWrapper = $element[ 0 ].getElementsByTagName('md-tabs-content-wrapper')[ 0 ]; 24271 elements.contents = elements.contentsWrapper.getElementsByTagName('md-tab-content'); 24272 24273 return elements; 24274 } 24275 24276 /** 24277 * Determines whether or not the left pagination arrow should be enabled. 24278 * @returns {boolean} 24279 */ 24280 function canPageBack () { 24281 return ctrl.offsetLeft > 0; 24282 } 24283 24284 /** 24285 * Determines whether or not the right pagination arrow should be enabled. 24286 * @returns {*|boolean} 24287 */ 24288 function canPageForward () { 24289 var lastTab = elements.tabs[ elements.tabs.length - 1 ]; 24290 return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth + 24291 ctrl.offsetLeft; 24292 } 24293 24294 /** 24295 * Determines if the UI should stretch the tabs to fill the available space. 24296 * @returns {*} 24297 */ 24298 function shouldStretchTabs () { 24299 switch (ctrl.stretchTabs) { 24300 case 'always': 24301 return true; 24302 case 'never': 24303 return false; 24304 default: 24305 return !ctrl.shouldPaginate 24306 && $window.matchMedia('(max-width: 600px)').matches; 24307 } 24308 } 24309 24310 /** 24311 * Determines if the tabs should appear centered. 24312 * @returns {string|boolean} 24313 */ 24314 function shouldCenterTabs () { 24315 return ctrl.centerTabs && !ctrl.shouldPaginate; 24316 } 24317 24318 /** 24319 * Determines if pagination is necessary to display the tabs within the available space. 24320 * @returns {boolean} 24321 */ 24322 function shouldPaginate () { 24323 if (ctrl.noPagination || !loaded) return false; 24324 var canvasWidth = $element.prop('clientWidth'); 24325 angular.forEach(getElements().dummies, function (tab) { canvasWidth -= tab.offsetWidth; }); 24326 return canvasWidth < 0; 24327 } 24328 24329 /** 24330 * Finds the nearest tab index that is available. This is primarily used for when the active 24331 * tab is removed. 24332 * @param newIndex 24333 * @returns {*} 24334 */ 24335 function getNearestSafeIndex (newIndex) { 24336 if (newIndex === -1) return -1; 24337 var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex), 24338 i, tab; 24339 for (i = 0; i <= maxOffset; i++) { 24340 tab = ctrl.tabs[ newIndex + i ]; 24341 if (tab && (tab.scope.disabled !== true)) return tab.getIndex(); 24342 tab = ctrl.tabs[ newIndex - i ]; 24343 if (tab && (tab.scope.disabled !== true)) return tab.getIndex(); 24344 } 24345 return newIndex; 24346 } 24347 24348 // Utility methods 24349 24350 /** 24351 * Defines a property using a getter and setter in order to trigger a change handler without 24352 * using `$watch` to observe changes. 24353 * @param key 24354 * @param handler 24355 * @param value 24356 */ 24357 function defineProperty (key, handler, value) { 24358 Object.defineProperty(ctrl, key, { 24359 get: function () { return value; }, 24360 set: function (newValue) { 24361 var oldValue = value; 24362 value = newValue; 24363 handler && handler(newValue, oldValue); 24364 } 24365 }); 24366 } 24367 24368 /** 24369 * Updates whether or not pagination should be displayed. 24370 */ 24371 function updatePagination () { 24372 if (!shouldStretchTabs()) updatePagingWidth(); 24373 ctrl.maxTabWidth = getMaxTabWidth(); 24374 ctrl.shouldPaginate = shouldPaginate(); 24375 } 24376 24377 function updatePagingWidth() { 24378 var width = 1; 24379 angular.forEach(getElements().dummies, function (element) { 24380 //-- Uses the larger value between `getBoundingClientRect().width` and `offsetWidth`. This 24381 // prevents `offsetWidth` value from being rounded down and causing wrapping issues, but 24382 // also handles scenarios where `getBoundingClientRect()` is inaccurate (ie. tabs inside 24383 // of a dialog) 24384 width += Math.max(element.offsetWidth, element.getBoundingClientRect().width); 24385 }); 24386 angular.element(elements.paging).css('width', Math.ceil(width) + 'px'); 24387 } 24388 24389 function getMaxTabWidth () { 24390 return $element.prop('clientWidth'); 24391 } 24392 24393 /** 24394 * Re-orders the tabs and updates the selected and focus indexes to their new positions. 24395 * This is triggered by `tabDirective.js` when the user's tabs have been re-ordered. 24396 */ 24397 function updateTabOrder () { 24398 var selectedItem = ctrl.tabs[ ctrl.selectedIndex ], 24399 focusItem = ctrl.tabs[ ctrl.focusIndex ]; 24400 ctrl.tabs = ctrl.tabs.sort(function (a, b) { 24401 return a.index - b.index; 24402 }); 24403 ctrl.selectedIndex = ctrl.tabs.indexOf(selectedItem); 24404 ctrl.focusIndex = ctrl.tabs.indexOf(focusItem); 24405 } 24406 24407 /** 24408 * This moves the selected or focus index left or right. This is used by the keydown handler. 24409 * @param inc 24410 */ 24411 function incrementIndex (inc, focus) { 24412 var newIndex, 24413 key = focus ? 'focusIndex' : 'selectedIndex', 24414 index = ctrl[ key ]; 24415 for (newIndex = index + inc; 24416 ctrl.tabs[ newIndex ] && ctrl.tabs[ newIndex ].scope.disabled; 24417 newIndex += inc) {} 24418 if (ctrl.tabs[ newIndex ]) { 24419 ctrl[ key ] = newIndex; 24420 } 24421 } 24422 24423 /** 24424 * This is used to forward focus to dummy elements. This method is necessary to avoid animation 24425 * issues when attempting to focus an item that is out of view. 24426 */ 24427 function redirectFocus () { 24428 getElements().dummies[ ctrl.focusIndex ].focus(); 24429 } 24430 24431 /** 24432 * Forces the pagination to move the focused tab into view. 24433 */ 24434 function adjustOffset (index) { 24435 if (index == null) index = ctrl.focusIndex; 24436 if (!elements.tabs[ index ]) return; 24437 if (ctrl.shouldCenterTabs) return; 24438 var tab = elements.tabs[ index ], 24439 left = tab.offsetLeft, 24440 right = tab.offsetWidth + left; 24441 ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth + 32 * 2)); 24442 ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left)); 24443 } 24444 24445 /** 24446 * Iterates through all queued functions and clears the queue. This is used for functions that 24447 * are called before the UI is ready, such as size calculations. 24448 */ 24449 function processQueue () { 24450 queue.forEach(function (func) { $mdUtil.nextTick(func); }); 24451 queue = []; 24452 } 24453 24454 /** 24455 * Determines if the tab content area is needed. 24456 */ 24457 function updateHasContent () { 24458 var hasContent = false; 24459 angular.forEach(ctrl.tabs, function (tab) { 24460 if (tab.template) hasContent = true; 24461 }); 24462 ctrl.hasContent = hasContent; 24463 } 24464 24465 /** 24466 * Moves the indexes to their nearest valid values. 24467 */ 24468 function refreshIndex () { 24469 ctrl.selectedIndex = getNearestSafeIndex(ctrl.selectedIndex); 24470 ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex); 24471 } 24472 24473 /** 24474 * Calculates the content height of the current tab. 24475 * @returns {*} 24476 */ 24477 function updateHeightFromContent () { 24478 if (!ctrl.dynamicHeight) return $element.css('height', ''); 24479 if (!ctrl.tabs.length) return queue.push(updateHeightFromContent); 24480 24481 var tabContent = elements.contents[ ctrl.selectedIndex ], 24482 contentHeight = tabContent ? tabContent.offsetHeight : 0, 24483 tabsHeight = elements.wrapper.offsetHeight, 24484 newHeight = contentHeight + tabsHeight, 24485 currentHeight = $element.prop('clientHeight'); 24486 24487 if (currentHeight === newHeight) return; 24488 24489 // Adjusts calculations for when the buttons are bottom-aligned since this relies on absolute 24490 // positioning. This should probably be cleaned up if a cleaner solution is possible. 24491 if ($element.attr('md-align-tabs') === 'bottom') { 24492 currentHeight -= tabsHeight; 24493 newHeight -= tabsHeight; 24494 // Need to include bottom border in these calculations 24495 if ($element.attr('md-border-bottom') !== undefined) ++currentHeight; 24496 } 24497 24498 // Lock during animation so the user can't change tabs 24499 locked = true; 24500 24501 var fromHeight = { height: currentHeight + 'px' }, 24502 toHeight = { height: newHeight + 'px' }; 24503 24504 // Set the height to the current, specific pixel height to fix a bug on iOS where the height 24505 // first animates to 0, then back to the proper height causing a visual glitch 24506 $element.css(fromHeight); 24507 24508 // Animate the height from the old to the new 24509 $animateCss($element, { 24510 from: fromHeight, 24511 to: toHeight, 24512 easing: 'cubic-bezier(0.35, 0, 0.25, 1)', 24513 duration: 0.5 24514 }).start().done(function () { 24515 // Then (to fix the same iOS issue as above), disable transitions and remove the specific 24516 // pixel height so the height can size with browser width/content changes, etc. 24517 $element.css({ 24518 transition: 'none', 24519 height: '' 24520 }); 24521 24522 // In the next tick, re-allow transitions (if we do it all at once, $element.css is "smart" 24523 // enough to batch it for us instead of doing it immediately, which undoes the original 24524 // transition: none) 24525 $mdUtil.nextTick(function() { 24526 $element.css('transition', ''); 24527 }); 24528 24529 // And unlock so tab changes can occur 24530 locked = false; 24531 }); 24532 } 24533 24534 /** 24535 * Repositions the ink bar to the selected tab. 24536 * @returns {*} 24537 */ 24538 function updateInkBarStyles () { 24539 if (!elements.tabs[ ctrl.selectedIndex ]) { 24540 angular.element(elements.inkBar).css({ left: 'auto', right: 'auto' }); 24541 return; 24542 } 24543 if (!ctrl.tabs.length) return queue.push(ctrl.updateInkBarStyles); 24544 // if the element is not visible, we will not be able to calculate sizes until it is 24545 // we should treat that as a resize event rather than just updating the ink bar 24546 if (!$element.prop('offsetParent')) return handleResizeWhenVisible(); 24547 var index = ctrl.selectedIndex, 24548 totalWidth = elements.paging.offsetWidth, 24549 tab = elements.tabs[ index ], 24550 left = tab.offsetLeft, 24551 right = totalWidth - left - tab.offsetWidth, 24552 tabWidth; 24553 if (ctrl.shouldCenterTabs) { 24554 tabWidth = Array.prototype.slice.call(elements.tabs).reduce(function (value, element) { 24555 return value + element.offsetWidth; 24556 }, 0); 24557 if (totalWidth > tabWidth) $mdUtil.nextTick(updateInkBarStyles, false); 24558 } 24559 updateInkBarClassName(); 24560 angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' }); 24561 } 24562 24563 /** 24564 * Adds left/right classes so that the ink bar will animate properly. 24565 */ 24566 function updateInkBarClassName () { 24567 var newIndex = ctrl.selectedIndex, 24568 oldIndex = ctrl.lastSelectedIndex, 24569 ink = angular.element(elements.inkBar); 24570 if (!angular.isNumber(oldIndex)) return; 24571 ink 24572 .toggleClass('md-left', newIndex < oldIndex) 24573 .toggleClass('md-right', newIndex > oldIndex); 24574 } 24575 24576 /** 24577 * Takes an offset value and makes sure that it is within the min/max allowed values. 24578 * @param value 24579 * @returns {*} 24580 */ 24581 function fixOffset (value) { 24582 if (!elements.tabs.length || !ctrl.shouldPaginate) return 0; 24583 var lastTab = elements.tabs[ elements.tabs.length - 1 ], 24584 totalWidth = lastTab.offsetLeft + lastTab.offsetWidth; 24585 value = Math.max(0, value); 24586 value = Math.min(totalWidth - elements.canvas.clientWidth, value); 24587 return value; 24588 } 24589 24590 /** 24591 * Attaches a ripple to the tab item element. 24592 * @param scope 24593 * @param element 24594 */ 24595 function attachRipple (scope, element) { 24596 var options = { colorElement: angular.element(elements.inkBar) }; 24597 $mdTabInkRipple.attach(scope, element, options); 24598 } 24599 } 24600 MdTabsController.$inject = ["$scope", "$element", "$window", "$mdConstant", "$mdTabInkRipple", "$mdUtil", "$animateCss", "$attrs", "$compile", "$mdTheming"]; 24601 24602 })(); 24603 (function(){ 24604 "use strict"; 24605 24606 /** 24607 * @ngdoc directive 24608 * @name mdTabs 24609 * @module material.components.tabs 24610 * 24611 * @restrict E 24612 * 24613 * @description 24614 * The `<md-tabs>` directive serves as the container for 1..n `<md-tab>` child directives to produces a Tabs components. 24615 * In turn, the nested `<md-tab>` directive is used to specify a tab label for the **header button** and a [optional] tab view 24616 * content that will be associated with each tab button. 24617 * 24618 * Below is the markup for its simplest usage: 24619 * 24620 * <hljs lang="html"> 24621 * <md-tabs> 24622 * <md-tab label="Tab #1"></md-tab> 24623 * <md-tab label="Tab #2"></md-tab> 24624 * <md-tab label="Tab #3"></md-tab> 24625 * </md-tabs> 24626 * </hljs> 24627 * 24628 * Tabs supports three (3) usage scenarios: 24629 * 24630 * 1. Tabs (buttons only) 24631 * 2. Tabs with internal view content 24632 * 3. Tabs with external view content 24633 * 24634 * **Tab-only** support is useful when tab buttons are used for custom navigation regardless of any other components, content, or views. 24635 * **Tabs with internal views** are the traditional usages where each tab has associated view content and the view switching is managed internally by the Tabs component. 24636 * **Tabs with external view content** is often useful when content associated with each tab is independently managed and data-binding notifications announce tab selection changes. 24637 * 24638 * Additional features also include: 24639 * 24640 * * Content can include any markup. 24641 * * If a tab is disabled while active/selected, then the next tab will be auto-selected. 24642 * 24643 * ### Explanation of tab stretching 24644 * 24645 * Initially, tabs will have an inherent size. This size will either be defined by how much space is needed to accommodate their text or set by the user through CSS. Calculations will be based on this size. 24646 * 24647 * On mobile devices, tabs will be expanded to fill the available horizontal space. When this happens, all tabs will become the same size. 24648 * 24649 * On desktops, by default, stretching will never occur. 24650 * 24651 * This default behavior can be overridden through the `md-stretch-tabs` attribute. Here is a table showing when stretching will occur: 24652 * 24653 * `md-stretch-tabs` | mobile | desktop 24654 * ------------------|-----------|-------- 24655 * `auto` | stretched | --- 24656 * `always` | stretched | stretched 24657 * `never` | --- | --- 24658 * 24659 * @param {integer=} md-selected Index of the active/selected tab 24660 * @param {boolean=} md-no-ink If present, disables ink ripple effects. 24661 * @param {boolean=} md-no-ink-bar If present, disables the selection ink bar. 24662 * @param {string=} md-align-tabs Attribute to indicate position of tab buttons: `bottom` or `top`; default is `top` 24663 * @param {string=} md-stretch-tabs Attribute to indicate whether or not to stretch tabs: `auto`, `always`, or `never`; default is `auto` 24664 * @param {boolean=} md-dynamic-height When enabled, the tab wrapper will resize based on the contents of the selected tab 24665 * @param {boolean=} md-border-bottom If present, shows a solid `1px` border between the tabs and their content 24666 * @param {boolean=} md-center-tabs When enabled, tabs will be centered provided there is no need for pagination 24667 * @param {boolean=} md-no-pagination When enabled, pagination will remain off 24668 * @param {boolean=} md-swipe-content When enabled, swipe gestures will be enabled for the content area to jump between tabs 24669 * @param {boolean=} md-enable-disconnect When enabled, scopes will be disconnected for tabs that are not being displayed. This provides a performance boost, but may also cause unexpected issues and is not recommended for most users. 24670 * @param {boolean=} md-autoselect When present, any tabs added after the initial load will be automatically selected 24671 * @param {boolean=} md-no-select-click When enabled, click events will not be fired when selecting tabs 24672 * 24673 * @usage 24674 * <hljs lang="html"> 24675 * <md-tabs md-selected="selectedIndex" > 24676 * <img ng-src="img/angular.png" class="centered"> 24677 * <md-tab 24678 * ng-repeat="tab in tabs | orderBy:predicate:reversed" 24679 * md-on-select="onTabSelected(tab)" 24680 * md-on-deselect="announceDeselected(tab)" 24681 * ng-disabled="tab.disabled"> 24682 * <md-tab-label> 24683 * {{tab.title}} 24684 * <img src="img/removeTab.png" ng-click="removeTab(tab)" class="delete"> 24685 * </md-tab-label> 24686 * <md-tab-body> 24687 * {{tab.content}} 24688 * </md-tab-body> 24689 * </md-tab> 24690 * </md-tabs> 24691 * </hljs> 24692 * 24693 */ 24694 angular 24695 .module('material.components.tabs') 24696 .directive('mdTabs', MdTabs); 24697 24698 function MdTabs () { 24699 return { 24700 scope: { 24701 selectedIndex: '=?mdSelected' 24702 }, 24703 template: function (element, attr) { 24704 attr[ "$mdTabsTemplate" ] = element.html(); 24705 return '' + 24706 '<md-tabs-wrapper> ' + 24707 '<md-tab-data></md-tab-data> ' + 24708 '<md-prev-button ' + 24709 'tabindex="-1" ' + 24710 'role="button" ' + 24711 'aria-label="Previous Page" ' + 24712 'aria-disabled="{{!$mdTabsCtrl.canPageBack()}}" ' + 24713 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageBack() }" ' + 24714 'ng-if="$mdTabsCtrl.shouldPaginate" ' + 24715 'ng-click="$mdTabsCtrl.previousPage()"> ' + 24716 '<md-icon md-svg-icon="md-tabs-arrow"></md-icon> ' + 24717 '</md-prev-button> ' + 24718 '<md-next-button ' + 24719 'tabindex="-1" ' + 24720 'role="button" ' + 24721 'aria-label="Next Page" ' + 24722 'aria-disabled="{{!$mdTabsCtrl.canPageForward()}}" ' + 24723 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageForward() }" ' + 24724 'ng-if="$mdTabsCtrl.shouldPaginate" ' + 24725 'ng-click="$mdTabsCtrl.nextPage()"> ' + 24726 '<md-icon md-svg-icon="md-tabs-arrow"></md-icon> ' + 24727 '</md-next-button> ' + 24728 '<md-tabs-canvas ' + 24729 'tabindex="{{ $mdTabsCtrl.hasFocus ? -1 : 0 }}" ' + 24730 'aria-activedescendant="tab-item-{{$mdTabsCtrl.tabs[$mdTabsCtrl.focusIndex].id}}" ' + 24731 'ng-focus="$mdTabsCtrl.redirectFocus()" ' + 24732 'ng-class="{ ' + 24733 '\'md-paginated\': $mdTabsCtrl.shouldPaginate, ' + 24734 '\'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs ' + 24735 '}" ' + 24736 'ng-keydown="$mdTabsCtrl.keydown($event)" ' + 24737 'role="tablist"> ' + 24738 '<md-pagination-wrapper ' + 24739 'ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs }" ' + 24740 'md-tab-scroll="$mdTabsCtrl.scroll($event)"> ' + 24741 '<md-tab-item ' + 24742 'tabindex="-1" ' + 24743 'class="md-tab" ' + 24744 'ng-repeat="tab in $mdTabsCtrl.tabs" ' + 24745 'role="tab" ' + 24746 'aria-controls="tab-content-{{::tab.id}}" ' + 24747 'aria-selected="{{tab.isActive()}}" ' + 24748 'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' + 24749 'ng-click="$mdTabsCtrl.select(tab.getIndex())" ' + 24750 'ng-class="{ ' + 24751 '\'md-active\': tab.isActive(), ' + 24752 '\'md-focused\': tab.hasFocus(), ' + 24753 '\'md-disabled\': tab.scope.disabled ' + 24754 '}" ' + 24755 'ng-disabled="tab.scope.disabled" ' + 24756 'md-swipe-left="$mdTabsCtrl.nextPage()" ' + 24757 'md-swipe-right="$mdTabsCtrl.previousPage()" ' + 24758 'md-tabs-template="::tab.label" ' + 24759 'md-scope="::tab.parent"></md-tab-item> ' + 24760 '<md-ink-bar></md-ink-bar> ' + 24761 '</md-pagination-wrapper> ' + 24762 '<div class="md-visually-hidden md-dummy-wrapper"> ' + 24763 '<md-dummy-tab ' + 24764 'class="md-tab" ' + 24765 'tabindex="-1" ' + 24766 'id="tab-item-{{::tab.id}}" ' + 24767 'role="tab" ' + 24768 'aria-controls="tab-content-{{::tab.id}}" ' + 24769 'aria-selected="{{tab.isActive()}}" ' + 24770 'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' + 24771 'ng-focus="$mdTabsCtrl.hasFocus = true" ' + 24772 'ng-blur="$mdTabsCtrl.hasFocus = false" ' + 24773 'ng-repeat="tab in $mdTabsCtrl.tabs" ' + 24774 'md-tabs-template="::tab.label" ' + 24775 'md-scope="::tab.parent"></md-dummy-tab> ' + 24776 '</div> ' + 24777 '</md-tabs-canvas> ' + 24778 '</md-tabs-wrapper> ' + 24779 '<md-tabs-content-wrapper ng-show="$mdTabsCtrl.hasContent && $mdTabsCtrl.selectedIndex >= 0"> ' + 24780 '<md-tab-content ' + 24781 'id="tab-content-{{::tab.id}}" ' + 24782 'role="tabpanel" ' + 24783 'aria-labelledby="tab-item-{{::tab.id}}" ' + 24784 'md-swipe-left="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(1)" ' + 24785 'md-swipe-right="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(-1)" ' + 24786 'ng-if="$mdTabsCtrl.hasContent" ' + 24787 'ng-repeat="(index, tab) in $mdTabsCtrl.tabs" ' + 24788 'ng-class="{ ' + 24789 '\'md-no-transition\': $mdTabsCtrl.lastSelectedIndex == null, ' + 24790 '\'md-active\': tab.isActive(), ' + 24791 '\'md-left\': tab.isLeft(), ' + 24792 '\'md-right\': tab.isRight(), ' + 24793 '\'md-no-scroll\': $mdTabsCtrl.dynamicHeight ' + 24794 '}"> ' + 24795 '<div ' + 24796 'md-tabs-template="::tab.template" ' + 24797 'md-connected-if="tab.isActive()" ' + 24798 'md-scope="::tab.parent" ' + 24799 'ng-if="$mdTabsCtrl.enableDisconnect || tab.shouldRender()"></div> ' + 24800 '</md-tab-content> ' + 24801 '</md-tabs-content-wrapper>'; 24802 }, 24803 controller: 'MdTabsController', 24804 controllerAs: '$mdTabsCtrl', 24805 bindToController: true 24806 }; 24807 } 24808 24809 })(); 24810 (function(){ 24811 "use strict"; 24812 24813 angular 24814 .module('material.components.tabs') 24815 .directive('mdTabsTemplate', MdTabsTemplate); 24816 24817 function MdTabsTemplate ($compile, $mdUtil) { 24818 return { 24819 restrict: 'A', 24820 link: link, 24821 scope: { 24822 template: '=mdTabsTemplate', 24823 connected: '=?mdConnectedIf', 24824 compileScope: '=mdScope' 24825 }, 24826 require: '^?mdTabs' 24827 }; 24828 function link (scope, element, attr, ctrl) { 24829 if (!ctrl) return; 24830 var compileScope = ctrl.enableDisconnect ? scope.compileScope.$new() : scope.compileScope; 24831 element.html(scope.template); 24832 $compile(element.contents())(compileScope); 24833 new MutationObserver(function() { 24834 ctrl.updatePagination(); 24835 ctrl.updateInkBarStyles(); 24836 }).observe(element[0], { childList: true, subtree: true } ); 24837 return $mdUtil.nextTick(handleScope); 24838 24839 function handleScope () { 24840 scope.$watch('connected', function (value) { value === false ? disconnect() : reconnect(); }); 24841 scope.$on('$destroy', reconnect); 24842 } 24843 24844 function disconnect () { 24845 if (ctrl.enableDisconnect) $mdUtil.disconnectScope(compileScope); 24846 } 24847 24848 function reconnect () { 24849 if (ctrl.enableDisconnect) $mdUtil.reconnectScope(compileScope); 24850 } 24851 } 24852 } 24853 MdTabsTemplate.$inject = ["$compile", "$mdUtil"]; 24854 24855 })(); 24856 (function(){ 24857 angular.module("material.core").constant("$MD_THEME_CSS", "md-autocomplete.md-THEME_NAME-theme { background: '{{background-50}}'; } md-autocomplete.md-THEME_NAME-theme[disabled] { background: '{{background-100}}'; } md-autocomplete.md-THEME_NAME-theme button md-icon path { fill: '{{background-600}}'; } md-autocomplete.md-THEME_NAME-theme button:after { background: '{{background-600-0.3}}'; }.md-autocomplete-suggestions-container.md-THEME_NAME-theme { background: '{{background-50}}'; } .md-autocomplete-suggestions-container.md-THEME_NAME-theme li { color: '{{background-900}}'; } .md-autocomplete-suggestions-container.md-THEME_NAME-theme li .highlight { color: '{{background-600}}'; } .md-autocomplete-suggestions-container.md-THEME_NAME-theme li:hover, .md-autocomplete-suggestions-container.md-THEME_NAME-theme li.selected { background: '{{background-200}}'; }md-backdrop { background-color: '{{background-900-0.0}}'; } md-backdrop.md-opaque.md-THEME_NAME-theme { background-color: '{{background-900-1.0}}'; }a.md-button.md-THEME_NAME-theme:not([disabled]):hover,.md-button.md-THEME_NAME-theme:not([disabled]):hover { background-color: '{{background-500-0.2}}'; }a.md-button.md-THEME_NAME-theme:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme:not([disabled]).md-focused { background-color: '{{background-500-0.2}}'; }a.md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover,.md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover { background-color: transparent; }a.md-button.md-THEME_NAME-theme.md-fab,.md-button.md-THEME_NAME-theme.md-fab { background-color: '{{accent-color}}'; color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab md-icon, .md-button.md-THEME_NAME-theme.md-fab md-icon { color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover { background-color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused { background-color: '{{accent-A700}}'; }a.md-button.md-THEME_NAME-theme.md-primary,.md-button.md-THEME_NAME-theme.md-primary { color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised, a.md-button.md-THEME_NAME-theme.md-primary.md-fab, .md-button.md-THEME_NAME-theme.md-primary.md-raised, .md-button.md-THEME_NAME-theme.md-primary.md-fab { color: '{{primary-contrast}}'; background-color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon { color: '{{primary-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover { background-color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused { background-color: '{{primary-600}}'; } a.md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon { color: '{{primary-color}}'; }a.md-button.md-THEME_NAME-theme.md-fab,.md-button.md-THEME_NAME-theme.md-fab { background-color: '{{accent-color}}'; color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]) .md-icon, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]) .md-icon { color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover { background-color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused { background-color: '{{accent-A700}}'; }a.md-button.md-THEME_NAME-theme.md-raised,.md-button.md-THEME_NAME-theme.md-raised { color: '{{background-900}}'; background-color: '{{background-50}}'; } a.md-button.md-THEME_NAME-theme.md-raised:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-raised:not([disabled]) md-icon { color: '{{background-900}}'; } a.md-button.md-THEME_NAME-theme.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-raised:not([disabled]):hover { background-color: '{{background-50}}'; } a.md-button.md-THEME_NAME-theme.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-raised:not([disabled]).md-focused { background-color: '{{background-200}}'; }a.md-button.md-THEME_NAME-theme.md-warn,.md-button.md-THEME_NAME-theme.md-warn { color: '{{warn-color}}'; } a.md-button.md-THEME_NAME-theme.md-warn.md-raised, a.md-button.md-THEME_NAME-theme.md-warn.md-fab, .md-button.md-THEME_NAME-theme.md-warn.md-raised, .md-button.md-THEME_NAME-theme.md-warn.md-fab { color: '{{warn-contrast}}'; background-color: '{{warn-color}}'; } a.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]) md-icon, a.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]) md-icon { color: '{{warn-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]):hover, a.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]):hover { background-color: '{{warn-color}}'; } a.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]).md-focused, a.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]).md-focused { background-color: '{{warn-700}}'; } a.md-button.md-THEME_NAME-theme.md-warn:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-warn:not([disabled]) md-icon { color: '{{warn-color}}'; }a.md-button.md-THEME_NAME-theme.md-accent,.md-button.md-THEME_NAME-theme.md-accent { color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme.md-accent.md-raised, a.md-button.md-THEME_NAME-theme.md-accent.md-fab, .md-button.md-THEME_NAME-theme.md-accent.md-raised, .md-button.md-THEME_NAME-theme.md-accent.md-fab { color: '{{accent-contrast}}'; background-color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]) md-icon, a.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]) md-icon { color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]):hover, a.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]):hover { background-color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]).md-focused, a.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]).md-focused { background-color: '{{accent-700}}'; } a.md-button.md-THEME_NAME-theme.md-accent:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-accent:not([disabled]) md-icon { color: '{{accent-color}}'; }a.md-button.md-THEME_NAME-theme[disabled], a.md-button.md-THEME_NAME-theme.md-raised[disabled], a.md-button.md-THEME_NAME-theme.md-fab[disabled], a.md-button.md-THEME_NAME-theme.md-accent[disabled], a.md-button.md-THEME_NAME-theme.md-warn[disabled],.md-button.md-THEME_NAME-theme[disabled],.md-button.md-THEME_NAME-theme.md-raised[disabled],.md-button.md-THEME_NAME-theme.md-fab[disabled],.md-button.md-THEME_NAME-theme.md-accent[disabled],.md-button.md-THEME_NAME-theme.md-warn[disabled] { color: '{{foreground-3}}' !important; cursor: default; } a.md-button.md-THEME_NAME-theme[disabled] md-icon, a.md-button.md-THEME_NAME-theme.md-raised[disabled] md-icon, a.md-button.md-THEME_NAME-theme.md-fab[disabled] md-icon, a.md-button.md-THEME_NAME-theme.md-accent[disabled] md-icon, a.md-button.md-THEME_NAME-theme.md-warn[disabled] md-icon, .md-button.md-THEME_NAME-theme[disabled] md-icon, .md-button.md-THEME_NAME-theme.md-raised[disabled] md-icon, .md-button.md-THEME_NAME-theme.md-fab[disabled] md-icon, .md-button.md-THEME_NAME-theme.md-accent[disabled] md-icon, .md-button.md-THEME_NAME-theme.md-warn[disabled] md-icon { color: '{{foreground-3}}'; }a.md-button.md-THEME_NAME-theme.md-raised[disabled], a.md-button.md-THEME_NAME-theme.md-fab[disabled],.md-button.md-THEME_NAME-theme.md-raised[disabled],.md-button.md-THEME_NAME-theme.md-fab[disabled] { background-color: '{{foreground-4}}'; }a.md-button.md-THEME_NAME-theme[disabled],.md-button.md-THEME_NAME-theme[disabled] { background-color: transparent; }md-bottom-sheet.md-THEME_NAME-theme { background-color: '{{background-50}}'; border-top-color: '{{background-300}}'; } md-bottom-sheet.md-THEME_NAME-theme.md-list md-list-item { color: '{{foreground-1}}'; } md-bottom-sheet.md-THEME_NAME-theme .md-subheader { background-color: '{{background-50}}'; } md-bottom-sheet.md-THEME_NAME-theme .md-subheader { color: '{{foreground-1}}'; }md-card.md-THEME_NAME-theme { background-color: '{{background-color}}'; border-radius: 2px; } md-card.md-THEME_NAME-theme .md-card-image { border-radius: 2px 2px 0 0; } md-card.md-THEME_NAME-theme md-card-header md-card-avatar md-icon { color: '{{background-color}}'; background-color: '{{foreground-3}}'; } md-card.md-THEME_NAME-theme md-card-header md-card-header-text .md-subhead { color: '{{foreground-2}}'; } md-card.md-THEME_NAME-theme md-card-title md-card-title-text:not(:only-child) .md-subhead { color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme .md-ripple { color: '{{accent-600}}'; }md-checkbox.md-THEME_NAME-theme.md-checked .md-ripple { color: '{{background-600}}'; }md-checkbox.md-THEME_NAME-theme.md-checked.md-focused .md-container:before { background-color: '{{accent-color-0.26}}'; }md-checkbox.md-THEME_NAME-theme .md-ink-ripple { color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme.md-checked .md-ink-ripple { color: '{{accent-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme .md-icon { border-color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme.md-checked .md-icon { background-color: '{{accent-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme.md-checked .md-icon:after { border-color: '{{accent-contrast-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary .md-ripple { color: '{{primary-600}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ripple { color: '{{background-600}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary .md-ink-ripple { color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple { color: '{{primary-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary .md-icon { border-color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-icon { background-color: '{{primary-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked.md-focused .md-container:before { background-color: '{{primary-color-0.26}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-icon:after { border-color: '{{primary-contrast-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn .md-ripple { color: '{{warn-600}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn .md-ink-ripple { color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple { color: '{{warn-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn .md-icon { border-color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-icon { background-color: '{{warn-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked.md-focused:not([disabled]) .md-container:before { background-color: '{{warn-color-0.26}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-icon:after { border-color: '{{background-200}}'; }md-checkbox.md-THEME_NAME-theme[disabled] .md-icon { border-color: '{{foreground-3}}'; }md-checkbox.md-THEME_NAME-theme[disabled].md-checked .md-icon { background-color: '{{foreground-3}}'; }md-checkbox.md-THEME_NAME-theme[disabled].md-checked .md-icon:after { border-color: '{{background-200}}'; }md-checkbox.md-THEME_NAME-theme[disabled] .md-label { color: '{{foreground-3}}'; }md-content.md-THEME_NAME-theme { color: '{{foreground-1}}'; background-color: '{{background-color}}'; }md-chips.md-THEME_NAME-theme .md-chips { box-shadow: 0 1px '{{background-300}}'; } md-chips.md-THEME_NAME-theme .md-chips.md-focused { box-shadow: 0 2px '{{primary-color}}'; }md-chips.md-THEME_NAME-theme .md-chip { background: '{{background-300}}'; color: '{{background-800}}'; } md-chips.md-THEME_NAME-theme .md-chip.md-focused { background: '{{primary-color}}'; color: '{{primary-contrast}}'; } md-chips.md-THEME_NAME-theme .md-chip.md-focused md-icon { color: '{{primary-contrast}}'; }md-chips.md-THEME_NAME-theme md-chip-remove .md-button md-icon path { fill: '{{background-500}}'; }.md-contact-suggestion span.md-contact-email { color: '{{background-400}}'; }/** Theme styles for mdCalendar. */.md-calendar.md-THEME_NAME-theme { color: '{{foreground-1}}'; } .md-calendar.md-THEME_NAME-theme tr:last-child td { border-bottom-color: '{{background-200}}'; }.md-THEME_NAME-theme .md-calendar-day-header { background: '{{background-hue-1}}'; color: '{{foreground-1}}'; }.md-THEME_NAME-theme .md-calendar-date.md-calendar-date-today .md-calendar-date-selection-indicator { border: 1px solid '{{primary-500}}'; }.md-THEME_NAME-theme .md-calendar-date.md-calendar-date-today.md-calendar-date-disabled { color: '{{primary-500-0.6}}'; }.md-THEME_NAME-theme .md-calendar-date.md-focus .md-calendar-date-selection-indicator { background: '{{background-hue-1}}'; }.md-THEME_NAME-theme .md-calendar-date-selection-indicator:hover { background: '{{background-hue-1}}'; }.md-THEME_NAME-theme .md-calendar-date.md-calendar-selected-date .md-calendar-date-selection-indicator,.md-THEME_NAME-theme .md-calendar-date.md-focus.md-calendar-selected-date .md-calendar-date-selection-indicator { background: '{{primary-500}}'; color: '{{primary-500-contrast}}'; border-color: transparent; }.md-THEME_NAME-theme .md-calendar-date-disabled,.md-THEME_NAME-theme .md-calendar-month-label-disabled { color: '{{foreground-3}}'; }/** Theme styles for mdDatepicker. */md-datepicker.md-THEME_NAME-theme { background: '{{background-color}}'; }.md-THEME_NAME-theme .md-datepicker-input { color: '{{background-contrast}}'; background: '{{background-color}}'; } .md-THEME_NAME-theme .md-datepicker-input::-webkit-input-placeholder, .md-THEME_NAME-theme .md-datepicker-input::-moz-placeholder, .md-THEME_NAME-theme .md-datepicker-input:-moz-placeholder, .md-THEME_NAME-theme .md-datepicker-input:-ms-input-placeholder { color: \"{{foreground-3}}\"; }.md-THEME_NAME-theme .md-datepicker-input-container { border-bottom-color: '{{background-300}}'; } .md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-focused { border-bottom-color: '{{primary-500}}'; } .md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-invalid { border-bottom-color: '{{warn-A700}}'; }.md-THEME_NAME-theme .md-datepicker-calendar-pane { border-color: '{{background-300}}'; }.md-THEME_NAME-theme .md-datepicker-triangle-button .md-datepicker-expand-triangle { border-top-color: '{{foreground-3}}'; }.md-THEME_NAME-theme .md-datepicker-triangle-button:hover .md-datepicker-expand-triangle { border-top-color: '{{foreground-2}}'; }.md-THEME_NAME-theme .md-datepicker-open .md-datepicker-calendar-icon { fill: '{{primary-500}}'; }.md-THEME_NAME-theme .md-datepicker-calendar,.md-THEME_NAME-theme .md-datepicker-input-mask-opaque { background: '{{background-color}}'; }md-dialog.md-THEME_NAME-theme { border-radius: 4px; background-color: '{{background-color}}'; } md-dialog.md-THEME_NAME-theme.md-content-overflow .md-actions, md-dialog.md-THEME_NAME-theme.md-content-overflow md-dialog-actions { border-top-color: '{{foreground-4}}'; }md-divider.md-THEME_NAME-theme { border-top-color: '{{foreground-4}}'; }.layout-row > md-divider.md-THEME_NAME-theme { border-right-color: '{{foreground-4}}'; }md-icon.md-THEME_NAME-theme { color: '{{foreground-2}}'; } md-icon.md-THEME_NAME-theme.md-primary { color: '{{primary-color}}'; } md-icon.md-THEME_NAME-theme.md-accent { color: '{{accent-color}}'; } md-icon.md-THEME_NAME-theme.md-warn { color: '{{warn-color}}'; }md-input-container.md-THEME_NAME-theme .md-input { color: '{{foreground-1}}'; border-color: '{{foreground-4}}'; text-shadow: '{{foreground-shadow}}'; } md-input-container.md-THEME_NAME-theme .md-input::-webkit-input-placeholder, md-input-container.md-THEME_NAME-theme .md-input::-moz-placeholder, md-input-container.md-THEME_NAME-theme .md-input:-moz-placeholder, md-input-container.md-THEME_NAME-theme .md-input:-ms-input-placeholder { color: \"{{foreground-3}}\"; }md-input-container.md-THEME_NAME-theme > md-icon { color: '{{foreground-1}}'; }md-input-container.md-THEME_NAME-theme label,md-input-container.md-THEME_NAME-theme .md-placeholder { text-shadow: '{{foreground-shadow}}'; color: '{{foreground-3}}'; }md-input-container.md-THEME_NAME-theme ng-messages, md-input-container.md-THEME_NAME-theme [ng-messages],md-input-container.md-THEME_NAME-theme ng-message, md-input-container.md-THEME_NAME-theme data-ng-message, md-input-container.md-THEME_NAME-theme x-ng-message,md-input-container.md-THEME_NAME-theme [ng-message], md-input-container.md-THEME_NAME-theme [data-ng-message], md-input-container.md-THEME_NAME-theme [x-ng-message],md-input-container.md-THEME_NAME-theme [ng-message-exp], md-input-container.md-THEME_NAME-theme [data-ng-message-exp], md-input-container.md-THEME_NAME-theme [x-ng-message-exp] { color: '{{warn-A700}}'; } md-input-container.md-THEME_NAME-theme ng-messages .md-char-counter, md-input-container.md-THEME_NAME-theme [ng-messages] .md-char-counter, md-input-container.md-THEME_NAME-theme ng-message .md-char-counter, md-input-container.md-THEME_NAME-theme data-ng-message .md-char-counter, md-input-container.md-THEME_NAME-theme x-ng-message .md-char-counter, md-input-container.md-THEME_NAME-theme [ng-message] .md-char-counter, md-input-container.md-THEME_NAME-theme [data-ng-message] .md-char-counter, md-input-container.md-THEME_NAME-theme [x-ng-message] .md-char-counter, md-input-container.md-THEME_NAME-theme [ng-message-exp] .md-char-counter, md-input-container.md-THEME_NAME-theme [data-ng-message-exp] .md-char-counter, md-input-container.md-THEME_NAME-theme [x-ng-message-exp] .md-char-counter { color: '{{foreground-1}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-has-value label { color: '{{foreground-2}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused .md-input { border-color: '{{primary-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused label { color: '{{primary-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused md-icon { color: '{{primary-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent .md-input { border-color: '{{accent-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent label { color: '{{accent-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn .md-input { border-color: '{{warn-A700}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn label { color: '{{warn-A700}}'; }md-input-container.md-THEME_NAME-theme.md-input-invalid .md-input { border-color: '{{warn-A700}}'; }md-input-container.md-THEME_NAME-theme.md-input-invalid.md-input-focused label { color: '{{warn-A700}}'; }md-input-container.md-THEME_NAME-theme.md-input-invalid ng-message, md-input-container.md-THEME_NAME-theme.md-input-invalid data-ng-message, md-input-container.md-THEME_NAME-theme.md-input-invalid x-ng-message,md-input-container.md-THEME_NAME-theme.md-input-invalid [ng-message], md-input-container.md-THEME_NAME-theme.md-input-invalid [data-ng-message], md-input-container.md-THEME_NAME-theme.md-input-invalid [x-ng-message],md-input-container.md-THEME_NAME-theme.md-input-invalid [ng-message-exp], md-input-container.md-THEME_NAME-theme.md-input-invalid [data-ng-message-exp], md-input-container.md-THEME_NAME-theme.md-input-invalid [x-ng-message-exp],md-input-container.md-THEME_NAME-theme.md-input-invalid .md-char-counter { color: '{{warn-A700}}'; }md-input-container.md-THEME_NAME-theme .md-input[disabled],md-input-container.md-THEME_NAME-theme .md-input [disabled] { border-bottom-color: transparent; color: '{{foreground-3}}'; background-image: linear-gradient(to right, \"{{foreground-3}}\" 0%, \"{{foreground-3}}\" 33%, transparent 0%); background-image: -ms-linear-gradient(left, transparent 0%, \"{{foreground-3}}\" 100%); }md-menu-content.md-THEME_NAME-theme { background-color: '{{background-color}}'; } md-menu-content.md-THEME_NAME-theme md-menu-divider { background-color: '{{foreground-4}}'; }md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text h3, md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text h4,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text h3,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text h4 { color: '{{foreground-1}}'; }md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text p,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text p { color: '{{foreground-2}}'; }md-list.md-THEME_NAME-theme .md-proxy-focus.md-focused div.md-no-style { background-color: '{{background-100}}'; }md-list.md-THEME_NAME-theme md-list-item > .md-avatar-icon { background-color: '{{foreground-3}}'; color: '{{background-color}}'; }md-list.md-THEME_NAME-theme md-list-item > md-icon { color: '{{foreground-2}}'; } md-list.md-THEME_NAME-theme md-list-item > md-icon.md-highlight { color: '{{primary-color}}'; } md-list.md-THEME_NAME-theme md-list-item > md-icon.md-highlight.md-accent { color: '{{accent-color}}'; }md-menu-bar.md-THEME_NAME-theme > button.md-button { color: '{{foreground-2}}'; border-radius: 2px; }md-menu-bar.md-THEME_NAME-theme md-menu.md-open > button, md-menu-bar.md-THEME_NAME-theme md-menu > button:focus { outline: none; background: '{{background-200}}'; }md-menu-bar.md-THEME_NAME-theme.md-open:not(.md-keyboard-mode) md-menu:hover > button { background-color: '{{ background-500-0.2}}'; }md-menu-bar.md-THEME_NAME-theme:not(.md-keyboard-mode):not(.md-open) md-menu button:hover,md-menu-bar.md-THEME_NAME-theme:not(.md-keyboard-mode):not(.md-open) md-menu button:focus { background: transparent; }md-menu-content.md-THEME_NAME-theme .md-menu > .md-button:after { color: '{{foreground-2}}'; }md-menu-content.md-THEME_NAME-theme .md-menu.md-open > .md-button { background-color: '{{ background-500-0.2}}'; }md-toolbar.md-THEME_NAME-theme.md-menu-toolbar { background-color: '{{background-color}}'; color: '{{foreground-1}}'; } md-toolbar.md-THEME_NAME-theme.md-menu-toolbar md-toolbar-filler { background-color: '{{primary-color}}'; color: '{{primary-contrast}}'; } md-toolbar.md-THEME_NAME-theme.md-menu-toolbar md-toolbar-filler md-icon { color: '{{primary-contrast}}'; }md-progress-circular.md-THEME_NAME-theme { background-color: transparent; } md-progress-circular.md-THEME_NAME-theme .md-inner .md-gap { border-top-color: '{{primary-color}}'; border-bottom-color: '{{primary-color}}'; } md-progress-circular.md-THEME_NAME-theme .md-inner .md-left .md-half-circle, md-progress-circular.md-THEME_NAME-theme .md-inner .md-right .md-half-circle { border-top-color: '{{primary-color}}'; } md-progress-circular.md-THEME_NAME-theme .md-inner .md-right .md-half-circle { border-right-color: '{{primary-color}}'; } md-progress-circular.md-THEME_NAME-theme .md-inner .md-left .md-half-circle { border-left-color: '{{primary-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-gap { border-top-color: '{{warn-color}}'; border-bottom-color: '{{warn-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-left .md-half-circle, md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-right .md-half-circle { border-top-color: '{{warn-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-right .md-half-circle { border-right-color: '{{warn-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-left .md-half-circle { border-left-color: '{{warn-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-gap { border-top-color: '{{accent-color}}'; border-bottom-color: '{{accent-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-left .md-half-circle, md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-right .md-half-circle { border-top-color: '{{accent-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-right .md-half-circle { border-right-color: '{{accent-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-left .md-half-circle { border-left-color: '{{accent-color}}'; }md-progress-linear.md-THEME_NAME-theme .md-container { background-color: '{{primary-100}}'; }md-progress-linear.md-THEME_NAME-theme .md-bar { background-color: '{{primary-color}}'; }md-progress-linear.md-THEME_NAME-theme.md-warn .md-container { background-color: '{{warn-100}}'; }md-progress-linear.md-THEME_NAME-theme.md-warn .md-bar { background-color: '{{warn-color}}'; }md-progress-linear.md-THEME_NAME-theme.md-accent .md-container { background-color: '{{accent-100}}'; }md-progress-linear.md-THEME_NAME-theme.md-accent .md-bar { background-color: '{{accent-color}}'; }md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-warn .md-bar1 { background-color: '{{warn-100}}'; }md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-warn .md-dashed:before { background: radial-gradient(\"{{warn-100}}\" 0%, \"{{warn-100}}\" 16%, transparent 42%); }md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-accent .md-bar1 { background-color: '{{accent-100}}'; }md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-accent .md-dashed:before { background: radial-gradient(\"{{accent-100}}\" 0%, \"{{accent-100}}\" 16%, transparent 42%); }md-radio-button.md-THEME_NAME-theme .md-off { border-color: '{{foreground-2}}'; }md-radio-button.md-THEME_NAME-theme .md-on { background-color: '{{accent-color-0.87}}'; }md-radio-button.md-THEME_NAME-theme.md-checked .md-off { border-color: '{{accent-color-0.87}}'; }md-radio-button.md-THEME_NAME-theme.md-checked .md-ink-ripple { color: '{{accent-color-0.87}}'; }md-radio-button.md-THEME_NAME-theme .md-container .md-ripple { color: '{{accent-600}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-on, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-on { background-color: '{{primary-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-off { border-color: '{{primary-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple { color: '{{primary-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-container .md-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-container .md-ripple { color: '{{primary-600}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-on, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-on { background-color: '{{warn-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-off { border-color: '{{warn-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple { color: '{{warn-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-container .md-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-container .md-ripple { color: '{{warn-600}}'; }md-radio-group.md-THEME_NAME-theme[disabled],md-radio-button.md-THEME_NAME-theme[disabled] { color: '{{foreground-3}}'; } md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-off, md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-off { border-color: '{{foreground-3}}'; } md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-on, md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-on { border-color: '{{foreground-3}}'; }md-radio-group.md-THEME_NAME-theme .md-checked .md-ink-ripple { color: '{{accent-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-primary .md-checked:not([disabled]) .md-ink-ripple, md-radio-group.md-THEME_NAME-theme .md-checked:not([disabled]).md-primary .md-ink-ripple { color: '{{primary-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme .md-checked.md-primary .md-ink-ripple { color: '{{warn-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked .md-container:before { background-color: '{{accent-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty).md-primary .md-checked .md-container:before,md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked.md-primary .md-container:before { background-color: '{{primary-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty).md-warn .md-checked .md-container:before,md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked.md-warn .md-container:before { background-color: '{{warn-color-0.26}}'; }md-select.md-THEME_NAME-theme[disabled] .md-select-value { border-bottom-color: transparent; background-image: linear-gradient(to right, \"{{foreground-3}}\" 0%, \"{{foreground-3}}\" 33%, transparent 0%); background-image: -ms-linear-gradient(left, transparent 0%, \"{{foreground-3}}\" 100%); }md-select.md-THEME_NAME-theme .md-select-value { border-bottom-color: '{{foreground-4}}'; } md-select.md-THEME_NAME-theme .md-select-value.md-select-placeholder { color: '{{foreground-3}}'; }md-select.md-THEME_NAME-theme.ng-invalid.ng-dirty .md-select-value { color: '{{warn-A700}}' !important; border-bottom-color: '{{warn-A700}}' !important; }md-select.md-THEME_NAME-theme:not([disabled]):focus .md-select-value { border-bottom-color: '{{primary-color}}'; color: '{{ foreground-1 }}'; } md-select.md-THEME_NAME-theme:not([disabled]):focus .md-select-value.md-select-placeholder { color: '{{ foreground-1 }}'; }md-select.md-THEME_NAME-theme:not([disabled]):focus.md-accent .md-select-value { border-bottom-color: '{{accent-color}}'; }md-select.md-THEME_NAME-theme:not([disabled]):focus.md-warn .md-select-value { border-bottom-color: '{{warn-color}}'; }md-select.md-THEME_NAME-theme[disabled] .md-select-value { color: '{{foreground-3}}'; } md-select.md-THEME_NAME-theme[disabled] .md-select-value.md-select-placeholder { color: '{{foreground-3}}'; }md-select-menu.md-THEME_NAME-theme md-option[disabled] { color: '{{foreground-3}}'; }md-select-menu.md-THEME_NAME-theme md-optgroup { color: '{{foreground-2}}'; } md-select-menu.md-THEME_NAME-theme md-optgroup md-option { color: '{{foreground-1}}'; }md-select-menu.md-THEME_NAME-theme md-option[selected] { color: '{{primary-500}}'; } md-select-menu.md-THEME_NAME-theme md-option[selected]:focus { color: '{{primary-600}}'; } md-select-menu.md-THEME_NAME-theme md-option[selected].md-accent { color: '{{accent-500}}'; } md-select-menu.md-THEME_NAME-theme md-option[selected].md-accent:focus { color: '{{accent-600}}'; }md-select-menu.md-THEME_NAME-theme md-option:focus:not([disabled]):not([selected]) { background: '{{background-200}}'; }md-sidenav.md-THEME_NAME-theme { background-color: '{{background-color}}'; }md-slider.md-THEME_NAME-theme .md-track { background-color: '{{foreground-3}}'; }md-slider.md-THEME_NAME-theme .md-track-ticks { background-color: '{{foreground-4}}'; }md-slider.md-THEME_NAME-theme .md-focus-thumb { background-color: '{{foreground-2}}'; }md-slider.md-THEME_NAME-theme .md-focus-ring { background-color: '{{accent-color}}'; }md-slider.md-THEME_NAME-theme .md-disabled-thumb { border-color: '{{background-color}}'; }md-slider.md-THEME_NAME-theme.md-min .md-thumb:after { background-color: '{{background-color}}'; }md-slider.md-THEME_NAME-theme .md-track.md-track-fill { background-color: '{{accent-color}}'; }md-slider.md-THEME_NAME-theme .md-thumb:after { border-color: '{{accent-color}}'; background-color: '{{accent-color}}'; }md-slider.md-THEME_NAME-theme .md-sign { background-color: '{{accent-color}}'; } md-slider.md-THEME_NAME-theme .md-sign:after { border-top-color: '{{accent-color}}'; }md-slider.md-THEME_NAME-theme .md-thumb-text { color: '{{accent-contrast}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-focus-ring { background-color: '{{warn-color}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-track.md-track-fill { background-color: '{{warn-color}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-thumb:after { border-color: '{{warn-color}}'; background-color: '{{warn-color}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-sign { background-color: '{{warn-color}}'; } md-slider.md-THEME_NAME-theme.md-warn .md-sign:after { border-top-color: '{{warn-color}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-thumb-text { color: '{{warn-contrast}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-focus-ring { background-color: '{{primary-color}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-track.md-track-fill { background-color: '{{primary-color}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-thumb:after { border-color: '{{primary-color}}'; background-color: '{{primary-color}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-sign { background-color: '{{primary-color}}'; } md-slider.md-THEME_NAME-theme.md-primary .md-sign:after { border-top-color: '{{primary-color}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-thumb-text { color: '{{primary-contrast}}'; }md-slider.md-THEME_NAME-theme[disabled] .md-thumb:after { border-color: '{{foreground-3}}'; }md-slider.md-THEME_NAME-theme[disabled]:not(.md-min) .md-thumb:after { background-color: '{{foreground-3}}'; }.md-subheader.md-THEME_NAME-theme { color: '{{ foreground-2-0.23 }}'; background-color: '{{background-color}}'; } .md-subheader.md-THEME_NAME-theme.md-primary { color: '{{primary-color}}'; } .md-subheader.md-THEME_NAME-theme.md-accent { color: '{{accent-color}}'; } .md-subheader.md-THEME_NAME-theme.md-warn { color: '{{warn-color}}'; }md-switch.md-THEME_NAME-theme .md-ink-ripple { color: '{{background-500}}'; }md-switch.md-THEME_NAME-theme .md-thumb { background-color: '{{background-50}}'; }md-switch.md-THEME_NAME-theme .md-bar { background-color: '{{background-500}}'; }md-switch.md-THEME_NAME-theme.md-checked .md-ink-ripple { color: '{{accent-color}}'; }md-switch.md-THEME_NAME-theme.md-checked .md-thumb { background-color: '{{accent-color}}'; }md-switch.md-THEME_NAME-theme.md-checked .md-bar { background-color: '{{accent-color-0.5}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-focused .md-thumb:before { background-color: '{{accent-color-0.26}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-primary .md-ink-ripple { color: '{{primary-color}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-primary .md-thumb { background-color: '{{primary-color}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-primary .md-bar { background-color: '{{primary-color-0.5}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-primary.md-focused .md-thumb:before { background-color: '{{primary-color-0.26}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-warn .md-ink-ripple { color: '{{warn-color}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-warn .md-thumb { background-color: '{{warn-color}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-warn .md-bar { background-color: '{{warn-color-0.5}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-warn.md-focused .md-thumb:before { background-color: '{{warn-color-0.26}}'; }md-switch.md-THEME_NAME-theme[disabled] .md-thumb { background-color: '{{background-400}}'; }md-switch.md-THEME_NAME-theme[disabled] .md-bar { background-color: '{{foreground-4}}'; }md-tabs.md-THEME_NAME-theme md-tabs-wrapper { background-color: transparent; border-color: '{{foreground-4}}'; }md-tabs.md-THEME_NAME-theme .md-paginator md-icon { color: '{{primary-color}}'; }md-tabs.md-THEME_NAME-theme md-ink-bar { color: '{{accent-color}}'; background: '{{accent-color}}'; }md-tabs.md-THEME_NAME-theme .md-tab { color: '{{foreground-2}}'; } md-tabs.md-THEME_NAME-theme .md-tab[disabled], md-tabs.md-THEME_NAME-theme .md-tab[disabled] md-icon { color: '{{foreground-3}}'; } md-tabs.md-THEME_NAME-theme .md-tab.md-active, md-tabs.md-THEME_NAME-theme .md-tab.md-active md-icon, md-tabs.md-THEME_NAME-theme .md-tab.md-focused, md-tabs.md-THEME_NAME-theme .md-tab.md-focused md-icon { color: '{{primary-color}}'; } md-tabs.md-THEME_NAME-theme .md-tab.md-focused { background: '{{primary-color-0.1}}'; } md-tabs.md-THEME_NAME-theme .md-tab .md-ripple-container { color: '{{accent-100}}'; }md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper { background-color: '{{accent-color}}'; } md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) { color: '{{accent-100}}'; } md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon { color: '{{accent-contrast}}'; } md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused { background: '{{accent-contrast-0.1}}'; } md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-ink-bar { color: '{{primary-600-1}}'; background: '{{primary-600-1}}'; }md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper { background-color: '{{primary-color}}'; } md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) { color: '{{primary-100}}'; } md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon { color: '{{primary-contrast}}'; } md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused { background: '{{primary-contrast-0.1}}'; }md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper { background-color: '{{warn-color}}'; } md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) { color: '{{warn-100}}'; } md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon { color: '{{warn-contrast}}'; } md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused { background: '{{warn-contrast-0.1}}'; }md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper { background-color: '{{primary-color}}'; } md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) { color: '{{primary-100}}'; } md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon { color: '{{primary-contrast}}'; } md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused { background: '{{primary-contrast-0.1}}'; }md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper { background-color: '{{accent-color}}'; } md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) { color: '{{accent-100}}'; } md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon { color: '{{accent-contrast}}'; } md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused { background: '{{accent-contrast-0.1}}'; } md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-ink-bar { color: '{{primary-600-1}}'; background: '{{primary-600-1}}'; }md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper { background-color: '{{warn-color}}'; } md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) { color: '{{warn-100}}'; } md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon { color: '{{warn-contrast}}'; } md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused { background: '{{warn-contrast-0.1}}'; }md-toast.md-THEME_NAME-theme .md-toast-content { background-color: #323232; color: '{{background-50}}'; } md-toast.md-THEME_NAME-theme .md-toast-content .md-button { color: '{{background-50}}'; } md-toast.md-THEME_NAME-theme .md-toast-content .md-button.md-highlight { color: '{{primary-A200}}'; } md-toast.md-THEME_NAME-theme .md-toast-content .md-button.md-highlight.md-accent { color: '{{accent-A200}}'; } md-toast.md-THEME_NAME-theme .md-toast-content .md-button.md-highlight.md-warn { color: '{{warn-A200}}'; }md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) { background-color: '{{primary-color}}'; color: '{{primary-contrast}}'; } md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-icon { color: '{{primary-contrast}}'; } md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) .md-button:not(.md-raised) { color: '{{primary-contrast}}'; } md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent { background-color: '{{accent-color}}'; color: '{{accent-contrast}}'; } md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn { background-color: '{{warn-color}}'; color: '{{warn-contrast}}'; }md-tooltip.md-THEME_NAME-theme { color: '{{background-A100}}'; } md-tooltip.md-THEME_NAME-theme .md-content { background-color: '{{foreground-2}}'; }"); 24858 })(); 24859 24860 24861 })(window, window.angular);;window.ngMaterial={version:{full: "1.0.7"}};